/* Canon Fidei - the studio.
   Editorial dark system. Restrained radii, thin borders, one rare accent,
   spacing doing the work instead of decoration. */

/* Satoshi, self-hosted. No Google Fonts request: the privacy page promises
   the site contacts no third party, and a webfont CDN would break that
   promise on every page load. Three weights, all three actually used - 400
   for body copy, 500 for the interface, 700 for the one place a heading
   needs it. Adding a fourth means adding the file; the browser will not
   synthesise from these, it will pick the nearest and let the weight lie. */
@font-face {
  font-family: 'Satoshi';
  src: url('assets/fonts/satoshi-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('assets/fonts/satoshi-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('assets/fonts/satoshi-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

/* ── Tokens ──────────────────────────────────────────────────────────────── */

:root {
  --paper:      #050505;
  --surface-1:  #0c0c0c;
  --surface-2:  #151515;
  --panel:       #0b0d12;
  --panel-raise: #15171d;
  --surface-3:  #1d1d1f;

  --ink:        #f5f5f3;
  --muted:      rgba(255, 255, 255, 0.66);
  /* Three text weights below --ink, and only three: on a #050505 ground a
     fourth, dimmer one cannot reach 4.5:1, and --faint (0.24) was sitting at
     2.00:1 across eleven rules - fineprint, form hints, placeholders, the
     footer's legal lines. Subordinate copy is separated by size here, not by
     dimming past the point of legibility. 0.47 is the lowest alpha that clears
     AA on every surface in this file; tools/check.js measures it. */
  --subtle:     rgba(255, 255, 255, 0.47);

  --line:        rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.15);

  --accent:      #2850ff;
  --rubric:      #dd7f7c;
  --verdigris:   #7fb3a8;
  --accent-soft: rgba(40, 80, 255, 0.14);
  --accent-line: rgba(40, 80, 255, 0.35);
  /* A highlighter stroke behind ink text: the terms excerpt marks the words
     that carry the promise. Paint only, never a text colour. */
  --highlight:   rgba(40, 80, 255, 0.34);

  --pending:      var(--rubric);
  --pending-soft: rgba(221, 127, 124, 0.09);
  --success:      var(--verdigris);
  --success-soft: rgba(127, 179, 168, 0.13);
  --success-line: rgba(127, 179, 168, 0.3);

  /* Restrained. Nothing here is a 30px pillow. */
  --r-xs:   4px;
  --r-sm:   6px;
  --r-md:  10px;
  --r-lg:  14px;
  --r-pill: 999px;

  --sans: 'Satoshi', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;

  --container: 1320px;
  --gutter: clamp(1.25rem, 4vw, 4rem);
  --measure: 40rem;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --t-fast: 200ms;
  --t-mid:  420ms;
  --t-slow: 760ms;

  /* Semantic pairs for the two themes. Everything that used to be a literal
     white-on-black lives here now, so a light theme is a token swap rather
     than a hunt through the stylesheet. */
  --on-ink:        #080808;
  --ink-hover:     #e9e9e6;
  --line-hover:    rgba(255, 255, 255, 0.32);
  --glass:         rgba(9, 9, 11, 0.90);
  --glass-solid:   rgba(9, 9, 11, 0.96);
  --glass-line:    rgba(255, 255, 255, 0.11);
  --shadow:        rgba(0, 0, 0, 0.4);
  --shadow-strong: rgba(0, 0, 0, 0.48);
  --atmos:         1;
  --mark-filter:   none;

  color-scheme: dark;
}

/* The light theme: the studio's own original light palette, cool stock and
   ink, not the brand sheet's warm paper, which AGENTS.md keeps for Bible
   Standard. The <head> script always writes the resolved theme onto <html>,
   so this is the only light block - no duplicate inside a media query - and a
   visitor with no JavaScript gets the designed dark site.

   Text tokens were solved, not picked: ink at 0.74 and 0.64 alpha are the
   values that clear AA on all four light surfaces with margin, and the rubric
   and verdigris are darkened until they do too. tools/check.js measures every
   one of them against every light surface on every run. */
:root[data-theme="light"] {
  --paper:      #f5f6f8;
  --surface-1:  #eef0f3;
  --surface-2:  #e6e9ed;
  --panel:       #eaedf2;
  --panel-raise: #f8f9fb;
  --surface-3:  #dde1e7;

  --ink:        #10151b;
  --muted:      rgba(16, 21, 27, 0.74);
  --subtle:     rgba(16, 21, 27, 0.64);

  --line:        rgba(16, 21, 27, 0.10);
  --line-strong: rgba(16, 21, 27, 0.18);

  --rubric:      #9e3d39;
  --verdigris:   #296b5f;
  --accent-soft: rgba(40, 80, 255, 0.10);
  --accent-line: rgba(40, 80, 255, 0.32);
  --highlight:   rgba(40, 80, 255, 0.16);

  --pending-soft: rgba(158, 61, 57, 0.08);
  --success-soft: rgba(41, 107, 95, 0.10);
  --success-line: rgba(41, 107, 95, 0.30);

  --on-ink:        #f5f6f8;
  --ink-hover:     #2a313a;
  --line-hover:    rgba(16, 21, 27, 0.34);
  --glass:         rgba(245, 246, 248, 0.90);
  --glass-solid:   rgba(245, 246, 248, 0.97);
  --glass-line:    rgba(16, 21, 27, 0.10);
  --shadow:        rgba(16, 21, 27, 0.10);
  --shadow-strong: rgba(16, 21, 27, 0.16);
  --atmos:         0.5;
  --mark-filter:   brightness(0);

  color-scheme: light;
}

/* ── Base ────────────────────────────────────────────────────────────────── */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}

::selection { background: var(--accent); color: #fff; }

.skip { position: absolute; left: -9999px; }
.skip:focus {
  left: 1.5rem; top: 1.5rem; z-index: 90;
  padding: 0.7rem 1.1rem;
  background: var(--ink); color: var(--on-ink);
  border-radius: var(--r-pill); font-weight: 500;
}

:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; border-radius: var(--r-xs); }

.wrap { width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }
.wrap-narrow { width: 100%; max-width: 52rem; margin: 0 auto; padding: 0 var(--gutter); }

/* clip, not hidden. The hero atmosphere is deliberately wider than the
   viewport, and once .hero stopped clipping it there was nothing left to
   contain it: overflow-x on body propagates to the viewport, so body never
   clips its own children. That let the canvas widen the document by 228px and
   every centred .wrap drifted left by half of it.

   clip contains it without creating a scroll container, so the sticky header
   keeps working; hidden here would break it. */
.shell {
  position: relative;
  isolation: isolate;
  overflow-x: clip;
  /* Reserved row tailored to the floating fixed header */
  padding-top: clamp(4.45rem, 5.8vw, 5.15rem);
}

section { position: relative; scroll-margin-top: 7rem; }

/* Editorial rhythm: sections are deliberately not evenly distributed. */
.band { padding: clamp(5rem, 11vw, 9rem) 0; }
.band-lg { padding: clamp(7rem, 15vw, 12.5rem) 0; }
.section-tight { padding: clamp(3rem, 6vw, 4.5rem) 0; }
.section-head { padding: clamp(7rem, 12vw, 9rem) 0 2rem; }

/* ── Typography ──────────────────────────────────────────────────────────── */

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 500;
  color: var(--ink);
  letter-spacing: -0.035em;
  line-height: 1.04;
  text-wrap: balance;
}

h1 { font-size: clamp(2.9rem, 5.4vw, 5.1rem); line-height: 1.0; letter-spacing: -0.045em; }
h2 { font-size: clamp(2.1rem, 3.6vw, 3.5rem); letter-spacing: -0.038em; line-height: 1.03; }
h3 { font-size: 1.25rem; letter-spacing: -0.022em; line-height: 1.25; }

p { margin: 0 0 1.1rem; color: var(--muted); max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

a { color: var(--ink); text-underline-offset: 3px; transition: color var(--t-fast) var(--ease); }
a:hover { color: var(--muted); }

.lede { font-size: clamp(1rem, 1.2vw, 1.14rem); line-height: 1.65; color: var(--muted); max-width: 34rem; }

.eyebrow {
  display: block;
  font-size: 0.75rem; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--subtle);
  margin-bottom: 1.4rem;
}

.eyebrow::before {
  content: "";
  display: block;
  width: 26px;
  height: 2px;
  background: var(--rubric);
  margin-bottom: 0.9rem;
}

.head-center .eyebrow::before { margin-inline: auto; }

/* The section's place in the page, the way a printed index numbers its
   chapters. Ink against the subtle label, so the number is read first. */
.eyebrow-num { margin-right: 0.9em; color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: 0.08em; }

.head-center { text-align: center; max-width: 46rem; margin: 0 auto clamp(3.5rem, 7vw, 5.5rem); }
.head-center .lede { margin: 1.15rem auto 0; }

/* ── Header ──────────────────────────────────────────────────────────────── */
/* Fixed positioning stopped the Safari jumping in manual QA. Preserve the
   original flat-at-top, rounded-on-scroll design by changing paint only:
   .stuck reveals the pill without moving or resizing the navigation.
   The shell above reserves its former place in document flow. */

.site-header {
  position: fixed;
  /* Fixed inset at top preserves Safari compositor stability during scroll */
  inset: 0.75rem 0 auto;
  z-index: 60;
  pointer-events: none;
  --nav-pad: clamp(1.15rem, 2vw, 1.75rem);
}

.site-header:has(.nav-links.open) {
  pointer-events: auto;
}

/* At the top, the bar is set on the page grid: its padding is taken out of
   the gutter, so the brand stands exactly over the hero headline. Scrolled,
   it condenses from that width into the pill. */
.nav-bar { width: 100%; padding-inline: max(0.5rem, calc(var(--gutter) - var(--nav-pad))); }

.nav-shell {
  position: relative;
  pointer-events: auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: clamp(0.85rem, 1.6vw, 1.5rem);
  height: clamp(3.95rem, 4.8vw, 4.35rem);
  /* Flat at the page grid's width at the top; once scrolled it condenses to
     a narrower pill. Width is the one geometric change the scrolled state is
     allowed (check.js); everything else about it is paint, and it uses no
     backdrop-filter and no transform, per Eddy's Safari fix. */
  max-width: calc(var(--container) - 2 * var(--gutter) + 2 * var(--nav-pad));
  margin: 0 auto;
  padding: 0 var(--nav-pad);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: transparent;
  box-shadow: none;
  transition: max-width 360ms cubic-bezier(0.16, 1, 0.3, 1),
              background 360ms cubic-bezier(0.16, 1, 0.3, 1),
              border-color 360ms cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow 360ms cubic-bezier(0.16, 1, 0.3, 1);
}

.site-header.stuck .nav-shell {
  max-width: min(67rem, calc(100vw - 2rem));
  background: var(--glass-solid);
  border-color: var(--glass-line);
  box-shadow: 0 20px 48px -12px rgba(0, 0, 0, 0.65),
              0 4px 14px -2px rgba(0, 0, 0, 0.35),
              inset 0 1px 1.5px 0 rgba(255, 255, 255, 0.28),
              inset 0 0 0 1px rgba(255, 255, 255, 0.05),
              inset 0 -1px 1px 0 rgba(0, 0, 0, 0.35);
}

:root[data-theme="light"] .site-header.stuck .nav-shell {
  box-shadow: 0 20px 44px -10px rgba(16, 21, 27, 0.12),
              0 4px 12px -2px rgba(16, 21, 27, 0.06),
              inset 0 1px 2px 0 rgba(255, 255, 255, 0.98),
              inset 0 0 0 1px rgba(255, 255, 255, 0.40),
              inset 0 -1px 1px 0 rgba(16, 21, 27, 0.05);
}

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

/* JavaScript adds .stuck after scroll. Without it, give the fixed navigation
   the same solid material at every scroll position so page content can never
   show through its labels. This selector deliberately does not affect the
   approved flat-at-top and rounded-on-scroll JavaScript states. */
html:not(.js) .nav-shell {
  background: var(--glass-solid);
  border-color: var(--glass-line);
  box-shadow: 0 1px 2px var(--shadow), 0 12px 32px var(--shadow);
}
/* The mark carries the brand on its own: white, no coloured tile behind it. */
.brand-link {
  display: inline-flex; align-items: center; min-height: 2.75rem; gap: 0.6rem;
  text-decoration: none; color: var(--ink);
  font-weight: 500; font-size: 1rem; letter-spacing: -0.015em;
  flex-shrink: 0;
  white-space: nowrap;
  border-radius: var(--r-sm);
}

.brand-mark {
  display: block;
  width: 1.15rem; height: 1.36rem;
  background: var(--ink);
  -webkit-mask: url('assets/canon-fidei-mark.svg') no-repeat center / contain;
  mask: url('assets/canon-fidei-mark.svg') no-repeat center / contain;
  flex-shrink: 0;
  transition: opacity var(--t-fast) var(--ease);
}

.brand-link:hover .brand-mark { opacity: 0.72; }

.nav-links {
  position: relative;
  display: flex; align-items: center; list-style: none;
  margin: 0; padding: 0.2rem;
  gap: clamp(0.4rem, 1.2vw, 1.4rem);
  white-space: nowrap;
}

.nav-indicator {
  position: absolute;
  top: 50%; left: 0;
  height: 2.15rem;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.4), inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transform: translateY(-50%) translateX(var(--indicator-x, 0));
  width: var(--indicator-w, 0);
  transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1),
              width 260ms cubic-bezier(0.16, 1, 0.3, 1);
}

:root[data-theme="light"] .nav-indicator {
  box-shadow: 0 2px 6px -1px rgba(16, 21, 27, 0.06), inset 0 1px 0 0 rgba(255, 255, 255, 0.85);
}

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

.nav-links a {
  position: relative;
  z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 2.5rem;
  min-width: 2.5rem;
  font-size: 0.875rem; color: var(--muted); text-decoration: none;
  padding: 0 clamp(0.55rem, 0.9vw, 0.8rem);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: transparent;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}

.nav-links a:hover { color: var(--ink); }

.nav-links a.active {
  /* This pill belongs to the current section and does not move on hover.
     The separate .nav-indicator can therefore preview another destination
     without erasing the visitor's position in the page. */
  color: var(--ink);
  font-weight: 500;
  background: var(--surface-2);
  border-color: var(--line);
}

.nav-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; white-space: nowrap; }

.nav-actions .btn:hover ~ .btn-arrow svg,
.nav-actions .btn-arrow:hover svg {
  transform: translate(2px, -2px);
}

.nav-actions .btn-arrow svg {
  transition: transform var(--t-fast) var(--ease);
}

.nav-theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem;
  min-width: 2.5rem; min-height: 2.5rem;
  padding: 0;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface-1);
  color: var(--muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.nav-theme-toggle:hover {
  color: var(--ink);
  border-color: var(--line-strong);
  background: var(--surface-2);
}

.nav-theme-toggle[hidden] {
  display: none;
}

.nav-theme-toggle svg {
  display: none;
  width: 1.15rem; height: 1.15rem;
}

.nav-theme-toggle[data-theme-active="system"] .theme-icon-system,
.nav-theme-toggle[data-theme-active="light"] .theme-icon-light,
.nav-theme-toggle[data-theme-active="dark"] .theme-icon-dark {
  display: block;
}

.brand-link:focus-visible,
.nav-links a:focus-visible,
.nav-toggle:focus-visible,
.nav-theme-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.nav-toggle {
  display: none; align-items: center; gap: 0.5rem;
  font: inherit; font-size: 0.8125rem; font-weight: 500;
  color: var(--ink); background: transparent;
  border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  padding: 0.55rem 0.9rem; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.nav-toggle-bars { display: grid; gap: 3px; width: 0.85rem; }
.nav-toggle-bars span { display: block; height: 1px; background: currentColor; transition: transform var(--t-mid) var(--ease), opacity var(--t-fast) var(--ease); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars span:nth-child(1) { transform: translateY(4px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars span:nth-child(3) { transform: translateY(-4px) rotate(-45deg); }
.nav-toggle-label { line-height: 1; }

.lang-switch { font-size: 0.75rem; font-weight: 500; color: var(--subtle); text-decoration: none; transition: color var(--t-fast) var(--ease); }
.lang-switch:hover { color: var(--ink); }

/* ── The phone nav ────────────────────────────────────────────────────────
   Three faults shipped with the detached pill and all three were only visible
   on a phone. The closed bar was two rows tall, because "Start a project"
   wrapped under the brand. The open menu had no background at the top of the
   page, so its links sat straight on the hero. And the 999px pill radius, fine
   on a one-line bar, turned the tall open panel into a clipped stadium.

   Now: closed is one row, brand and Menu. Open is a solid panel with an
   ordinary radius, the links run full width, and the project CTA moves inside
   it as a full-width button - the hero carries its own CTAs directly below,
   so nothing is lost by taking it out of the closed bar. */
@media (max-width: 62rem) {
  /* The menu opens over the page, not into it. The header keeps the height of
     the closed bar and the bar is positioned inside it, so however tall the
     open panel grows it drops over the hero instead of pushing the hero down. */
  .site-header { height: 4.35rem; }
  .nav-bar { position: relative; height: 100%; }
  .nav-shell {
    position: absolute; top: 0.45rem; left: var(--gutter); right: var(--gutter);
    width: auto; max-width: none; margin: 0;
  }

  /* Everything behind the open panel dims, and a tap on it closes the menu
     (site.js). Drawn inside the header's own stacking context, so it sits over
     the page but under the panel. */
  .site-header:has(.nav-links.open)::before {
    content: ""; position: fixed; inset: 0; z-index: -1;
    background: rgba(0, 0, 0, 0.5);
    animation: scrim-in var(--t-mid) var(--ease);
  }

  .nav-shell { flex-wrap: wrap; height: auto; padding: 0.55rem clamp(0.6rem, 3vw, 1rem) 0.55rem clamp(0.9rem, 4vw, 1.4rem); gap: 0 1rem; }

  /* Thumb-sized: both controls were under 40px. */
  .brand-link { min-height: 2.75rem; }
  .nav-toggle { min-height: 2.75rem; padding: 0 1rem; }

  /* The flex child is the <nav>, not the list inside it, so that is what has
     to take the full row. Sizing the <ul> left it 100% of a narrow parent. */
  .nav-shell > nav { order: 3; width: 100%; }

  .nav-indicator { display: none; }

  .nav-links {
    flex-direction: column; align-items: stretch;
    gap: 0; margin-top: 0.6rem; padding: 0.25rem 0 0 0;
    border-top: 1px solid var(--line);
  }
  .nav-links a { display: flex; align-items: center; min-height: 3rem; padding: 0 0.15rem; font-size: 1rem; border-bottom: 1px solid var(--line); border-radius: 0; }
  .nav-links a.active { color: var(--ink); font-weight: 500; }
  .nav-links li:last-child a { border-bottom: 0; }

  .js .nav-toggle { display: inline-flex; }
  .js .nav-links { display: none; }
  .js .nav-links.open { display: flex; }
  .js .nav-links.open li { animation: nav-in var(--t-mid) var(--ease) backwards; animation-delay: calc(var(--i, 0) * 40ms); }

  /* The project CTA and theme switch live in the open menu, below the links. */
  .js .nav-actions { display: none; }
  .nav-shell:has(.nav-links.open) .nav-actions {
    display: flex; order: 4; width: 100%;
    padding: 0.9rem 0 0.4rem;
    gap: 0.75rem;
    align-items: center;
    justify-content: space-between;
  }
  .nav-shell:has(.nav-links.open) .nav-actions .btn { flex: 1; justify-content: center; }

  /* Open is always a panel, scrolled or not: solid material behind the links,
     and a radius that suits a tall box instead of a one-line pill. */
  .nav-shell:has(.nav-links.open),
  html:not(.js) .nav-shell {
    background: var(--glass-solid);
    border-color: var(--glass-line);
    border-radius: var(--r-lg);
    box-shadow: 0 12px 32px var(--shadow);
  }
}

@keyframes nav-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

@keyframes scrim-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

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

/* The generic button row, unrelated to the closing CTA below. */
.cta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 2rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: 2.6rem; padding: 0 1.15rem;
  font: inherit; font-size: 0.85rem; font-weight: 500; line-height: 1;
  text-decoration: none;
  border-radius: var(--r-pill);
  background: var(--ink); color: var(--on-ink);
  border: 1px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease);
}

.btn:hover { background: var(--ink-hover); color: var(--on-ink); }
.btn:active { opacity: 0.85; }

/* The arrow is its own circular control beside the label, not an icon glued
   inside it. .pair makes the two read as one object.

   On hover the arrow leaves the circle in the direction it points and a second
   one arrives from behind, so the control reads as motion rather than as an
   icon that nudged two pixels. The duplicate is cloned by site.js rather than
   written into every button in the markup: there are eleven of these across
   the site, the duplicate is decoration with no meaning to a screen reader,
   and without JavaScript the single arrow simply sits still.

   The old rule eased `background` and `transform` but not `color`, so the
   glyph snapped to its hover colour while the disc was still travelling. */
.btn-arrow {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.6rem; height: 2.6rem;
  border-radius: 50%;
  background: var(--ink); color: var(--on-ink);
  border: 1px solid transparent;
  text-decoration: none; flex-shrink: 0;
  /* --mx and --my are the magnetic pull toward the pointer, set by site.js
     with a fine pointer only. translate, not transform, so the pull and any
     hover lift never overwrite each other. */
  translate: var(--mx, 0px) var(--my, 0px);
  transition: background var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              transform var(--t-mid) var(--ease),
              translate 520ms var(--ease);
}

.btn-arrow svg { width: 0.85rem; height: 0.85rem; }

.btn-arrow:hover,
.btn-arrow:focus-visible { background: var(--ink-hover); color: var(--on-ink); }

.pair { display: inline-flex; align-items: center; gap: 0.35rem; }
.pair:hover .btn,
.pair:focus-within .btn { background: var(--ink-hover); color: var(--on-ink); }
.pair:hover .btn-arrow,
.pair:focus-within .btn-arrow { background: var(--ink-hover); color: var(--on-ink); }

/* --- The slide -----------------------------------------------------------
   Only applied once site.js has cloned the glyph and marked the control, so a
   page with one arrow in it never lays that arrow out absolutely and loses it.
   Keyboard focus drives exactly the same motion as hover; an interaction only
   a mouse can reach is half an interaction. */
.arrow-swap svg {
  position: absolute;
  transition: transform var(--t-mid) var(--ease);
}
.arrow-swap svg + svg { transform: translateX(-190%); }

.btn-arrow.arrow-swap:hover svg,
.btn-arrow.arrow-swap:focus-visible svg,
.pair:hover .arrow-swap svg,
.pair:focus-within .arrow-swap svg { transform: translateX(190%); }

.btn-arrow.arrow-swap:hover svg + svg,
.btn-arrow.arrow-swap:focus-visible svg + svg,
.pair:hover .arrow-swap svg + svg,
.pair:focus-within .arrow-swap svg + svg { transform: translateX(0); }

/* The up-and-right glyph leaves along the diagonal it points down. */
.arrow-swap.arrow-ne svg + svg { transform: translate(-190%, 190%); }

.btn-arrow.arrow-ne:hover svg,
.btn-arrow.arrow-ne:focus-visible svg,
.pair:hover .arrow-ne svg,
.pair:focus-within .arrow-ne svg { transform: translate(190%, -190%); }

.btn-arrow.arrow-ne:hover svg + svg,
.btn-arrow.arrow-ne:focus-visible svg + svg,
.pair:hover .arrow-ne svg + svg,
.pair:focus-within .arrow-ne svg + svg { transform: translate(0, 0); }

/* A visitor who asked for less motion gets the colour change and nothing that
   travels: site.js never clones the glyph or adds .arrow-swap in that case, so
   there is nothing here to switch off. Suppressing it from this end would mean
   out-shouting six hover selectors with !important, which is the wrong way
   round - the enhancement should not be applied, not applied and then undone. */

.btn.ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
.btn.ghost:hover { background: var(--surface-2); border-color: var(--line-strong); color: var(--ink); }

.btn.secondary { background: var(--surface-2); color: var(--ink); border-color: var(--line); }
.btn.secondary:hover { background: var(--surface-3); color: var(--ink); }

/* ── Motion ──────────────────────────────────────────────────────────────── */

[data-reveal] { opacity: 1; }

.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
  transition-delay: calc(var(--i, 0) * 70ms);
  animation: reveal-failsafe 1ms linear 4s forwards;
}

.js [data-reveal].in { opacity: 1; transform: none; animation: none; }

/* The failsafe above shows everything four seconds after load, for the
   visitor whose script never arrives. Once site.js is running it marks the
   page reveal-live and the failsafe is withdrawn, so a section reached after
   those four seconds still arrives instead of already being there. */
.js.reveal-live [data-reveal]:not(.in) { animation: none; }

@keyframes reveal-failsafe { to { opacity: 1; transform: none; } }

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

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

/* Centred, and set as a statement: one line of facts in a glass pill, the
   headline at display scale in two authored lines, the reason under it and
   the two actions under that, all on the axis the collage and the shader
   light are built around. The facts are each true today; one is Paris's
   actual time, filled in by site.js and absent without it. */
.hero {
  position: relative;
  padding: clamp(2rem, 5vw, 4.25rem) 0 clamp(4rem, 8vw, 6rem);
  text-align: center;
}

.hero > .wrap { display: flex; flex-direction: column; align-items: center; }

.hero-meta {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.35rem 0;
  margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
  padding: 0.5rem 1.1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.hero-meta-item { display: inline-flex; align-items: center; gap: 0.55rem; }

/* A hairline between facts, drawn rather than typed, so it never wraps onto
   a line of its own. */
.hero-meta-item + .hero-meta-item::before {
  content: "";
  width: 1px;
  height: 0.85rem;
  margin-inline: 0.9rem 0.35rem;
  background: var(--line-strong);
}

.clock { color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: 0.06em; }
.clock[hidden] { display: none; }

.hero-title {
  font-size: clamp(2.75rem, 6.3vw, 6rem);
  line-height: 0.98;
  letter-spacing: -0.045em;
}

.hero-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: clamp(1.4rem, 2.6vw, 1.9rem);
}

.hero .lede { margin: 0 auto; max-width: 36rem; }
.hero .cta-row { justify-content: center; gap: 0.75rem; }

@media (max-width: 48rem) {
  .hero-meta-item:last-child { display: none; }
}

/* On a phone the pill keeps the one fact that changes: where, and what time
   it is there. */
@media (max-width: 34rem) {
  .hero-meta-item:first-child { display: none; }
  .hero-meta-item:nth-child(2)::before { display: none; }
}

.hero .cta-row .btn,
.hero .cta-row .btn-arrow {
  box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.24);
  transition: background var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
:root[data-theme="light"] .hero .cta-row .btn,
:root[data-theme="light"] .hero .cta-row .btn-arrow {
  box-shadow: 0 4px 12px -2px rgba(16, 21, 27, 0.12);
}
.hero .cta-row .pair:hover .btn,
.hero .cta-row .pair:hover .btn-arrow {
  transform: translateY(-1px);
  box-shadow: 0 6px 20px -2px rgba(0, 0, 0, 0.32);
}
.hero .cta-row .btn.ghost {
  box-shadow: none;
  background: var(--surface-1);
}
.hero .cta-row .btn.ghost:hover {
  transform: translateY(-1px);
  background: var(--surface-2);
}

/* The blue horizon: layered gradients and blur behind the collage. An
   atmosphere, not a neon glow. */
.horizon {
  opacity: var(--atmos);
  position: absolute;
  left: 50%; bottom: -12%;
  width: 145%; height: 30rem;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 0;
}

.horizon::before, .horizon::after {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 50%;
}

.horizon::before {
  width: 100%; height: 26rem; top: 3rem;
  background: radial-gradient(closest-side, rgba(40, 80, 255, 0.5) 0%, rgba(40, 80, 255, 0.14) 45%, transparent 78%);
  filter: blur(60px);
}

.horizon::after {
  width: 62%; height: 15rem; top: 6.5rem;
  background: radial-gradient(closest-side, rgba(150, 180, 255, 0.5) 0%, rgba(40, 80, 255, 0.3) 38%, transparent 72%);
  filter: blur(34px);
}

.hero-media { position: relative; align-self: stretch; margin-top: clamp(3.5rem, 7vw, 5.5rem); }

/* The shader canvas. It sits over the CSS horizon and fades in only once
   WebGL has actually produced a frame, so a browser without it keeps the
   gradient underneath and nothing looks broken. Wider than the container on
   purpose: the atmosphere should have no visible edge. */
.hero-shader {
  position: absolute;
  left: 50%;
  top: 0;
  width: 132%;
  height: 44rem;
  transform: translateX(-50%);
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.4s var(--ease);
}

.hero-shader.on { opacity: 1; }

/* With the shader running, the CSS horizon drops back to a faint floor so the
   two do not stack into something twice as bright as either. */
.shell:has(.hero-shader.on) .horizon { opacity: calc(var(--atmos) * 0.4); }

/* An overlapping cluster of real screenshots, art-directed rather than tiled.
   Supports multi-plane stereoscopic parallax across .edge, .mid and .lead layers. */
.collage {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  min-height: clamp(14rem, 26vw, 24rem);
  perspective: 1200px;
  perspective-origin: center 40%;
}

.collage figure {
  position: relative; margin: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: 0 28px 80px var(--shadow-strong);
  transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow 0.6s cubic-bezier(0.16, 1, 0.3, 1),
              border-color 0.4s ease,
              opacity 0.4s ease;
  will-change: transform;
}

.collage figure::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.14);
}

:root[data-theme="light"] .collage figure::after {
  box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.75);
}

.collage img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center; }

.collage .lead {
  width: 36%;
  z-index: 3;
  transform: translate3d(calc(var(--parallax-x, 0px) * 0.48), calc(var(--parallax-y, 0px) * 0.48), 24px);
}

.collage .mid {
  width: 26%;
  z-index: 2;
}

.collage .edge {
  width: 21%;
  z-index: 1;
  opacity: 0.66;
}

.collage .mid.left {
  margin-right: -4%;
  transform: translate3d(calc(var(--parallax-x, 0px) * 0.26), calc(1.5rem + var(--parallax-y, 0px) * 0.26), 0px) rotate(-1deg);
}
.collage .mid.right {
  margin-left: -4%;
  transform: translate3d(calc(var(--parallax-x, 0px) * 0.26), calc(1.5rem + var(--parallax-y, 0px) * 0.26), 0px) rotate(1deg);
}
.collage .edge.left {
  margin-right: -3%;
  transform: translate3d(calc(var(--parallax-x, 0px) * 0.12), calc(3rem + var(--parallax-y, 0px) * 0.12), -24px) rotate(-1.5deg);
}
.collage .edge.right {
  margin-left: -3%;
  transform: translate3d(calc(var(--parallax-x, 0px) * 0.12), calc(3rem + var(--parallax-y, 0px) * 0.12), -24px) rotate(1.5deg);
}

@media (hover: hover) {
  .collage figure:hover {
    border-color: var(--line-hover);
    box-shadow: 0 38px 96px var(--shadow-strong);
    z-index: 5;
  }
  .collage .lead:hover {
    transform: translate3d(calc(var(--parallax-x, 0px) * 0.48), calc(var(--parallax-y, 0px) * 0.48 - 8px), 42px) scale(1.02);
  }
  .collage .mid.left:hover {
    transform: translate3d(calc(var(--parallax-x, 0px) * 0.26), calc(1.5rem + var(--parallax-y, 0px) * 0.26 - 6px), 16px) rotate(-0.5deg) scale(1.02);
  }
  .collage .mid.right:hover {
    transform: translate3d(calc(var(--parallax-x, 0px) * 0.26), calc(1.5rem + var(--parallax-y, 0px) * 0.26 - 6px), 16px) rotate(0.5deg) scale(1.02);
  }
  .collage .edge.left:hover {
    opacity: 0.96;
    transform: translate3d(calc(var(--parallax-x, 0px) * 0.12), calc(3rem + var(--parallax-y, 0px) * 0.12 - 4px), -4px) rotate(-1deg) scale(1.02);
  }
  .collage .edge.right:hover {
    opacity: 0.96;
    transform: translate3d(calc(var(--parallax-x, 0px) * 0.12), calc(3rem + var(--parallax-y, 0px) * 0.12 - 4px), -4px) rotate(1deg) scale(1.02);
  }
}

@media (max-width: 60rem) {
  .collage { perspective: none; }
  .collage .edge { display: none; }
  .collage .lead { width: 46%; transform: none; }
  .collage .mid { width: 32%; }
  .collage .mid.left { transform: translateY(1.5rem) rotate(-1deg); }
  .collage .mid.right { transform: translateY(1.5rem) rotate(1deg); }
}

@media (max-width: 40rem) {
  .collage .mid.right { display: none; }
  .collage .lead { width: 62%; transform: none; }
  .collage .mid { width: 40%; }
  .collage .mid.left { transform: translateY(1.5rem) rotate(-1deg); }
}

/* ── Trust strip ─────────────────────────────────────────────────────────── */
/* Where a reference site puts a client logo wall. These are not clients - two
   employers, the two engineering consultancies involved, and the studio's own
   shop - and the line above the wall says so. Other companies' marks under a
   "clients" or "trusted by" heading would claim an endorsement nobody gave, and
   it is the first thing on the page a sceptical prospect would check.

   Every mark is reduced to white by tools/logos.js, so the wall reads as one
   object in the site's palette instead of five brand guidelines side by side. */

.trust { text-align: center; }
.trust-badge-row { display: flex; justify-content: center; margin-bottom: 0.85rem; }
.trust-badge {
  display: inline-block;
  text-transform: uppercase;
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  color: var(--subtle);
  font-variant-numeric: tabular-nums;
}
.trust-line { font-size: 0.8125rem; color: var(--subtle); margin: 0 auto 2.25rem; max-width: 40rem; }
.trust-line a { color: var(--muted); text-underline-offset: 3px; margin-left: 0.35rem; }
.trust-line a:hover { color: var(--ink); }

/* A ruled strip, not a floating row: hairlines above, below and between, the
   same device as every other list on the site. */
.logo-wall {
  --wall: 1;
  display: flex; flex-wrap: wrap; justify-content: center;
  list-style: none; margin: 0; padding: 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.logo-wall li {
  position: relative;
  flex: 1 1 0;
  display: flex; align-items: center; justify-content: center;
  min-height: clamp(6.5rem, 10vw, 8.5rem);
  padding: 1.25rem 1.5rem;
  transition: background var(--t-mid) var(--ease);
}
.logo-wall li + li { border-left: 1px solid var(--line); }
.logo-wall li:hover { background: var(--surface-1); }

.logo {
  display: block; width: auto; max-width: 100%; object-fit: contain;
  filter: var(--mark-filter);
  height: calc(var(--lh) * var(--wall));
  opacity: 0.62;
  transition: opacity var(--t-mid) var(--ease), transform var(--t-mid) var(--ease);
}

/* Optical sizing: equal visual area, not equal height. A 7:1 wordmark set as
   tall as a 1.7:1 mark reads four times larger, so height is 60 / sqrt(ratio)
   then corrected by eye for weight - dense bold marks down, thin ones up. */
.logo-amadeus    { --lh: 1.25rem; }
.logo-lexisnexis { --lh: 1.95rem; }
.logo-fortil     { --lh: 1.85rem; }
.logo-klanik     { --lh: 2rem; }
.logo-rr         { --lh: 4rem; }

/* Reading one mark quiets the others. Nothing is hidden behind the hover; it
   only changes which mark has the eye. */
.logo-wall:hover .logo { opacity: 0.3; }
.logo-wall li:hover .logo { opacity: 1; transform: translateY(-2px) scale(1.02); }

@media (max-width: 62rem) {
  .logo-wall { --wall: 0.86; }
  .logo-wall li { flex: 0 0 33.333%; }
  .logo-wall li + li { border-left: 0; }
}

@media (max-width: 34rem) {
  .logo-wall { --wall: 0.8; }
  .logo-wall li { flex-basis: 50%; min-height: 5.5rem; padding: 1rem 0.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  .logo-wall li:hover .logo { transform: none; }
}

/* ── Manifesto ───────────────────────────────────────────────────────────── */

.manifesto { display: grid; grid-template-columns: 12rem 1fr; gap: clamp(2rem, 5vw, 4rem); }
.manifesto-index { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--subtle); padding-top: 0.65rem; }

.manifesto-copy {
  font-size: clamp(1.35rem, 2.5vw, 2.05rem);
  line-height: 1.4; letter-spacing: -0.022em;
  color: var(--ink); max-width: 54rem; margin: 0; font-weight: 400;
}

.manifesto-copy .dim { color: var(--subtle); }

/* Terminal Industries scroll text scrub illumination */
.scrub-word {
  display: inline;
  color: var(--subtle);
  opacity: 0.26;
  transition: opacity 0.24s cubic-bezier(0.16, 1, 0.3, 1),
              color 0.24s cubic-bezier(0.16, 1, 0.3, 1),
              text-shadow 0.24s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, color;
}

.scrub-word.lit {
  color: var(--ink);
  opacity: 1;
  text-shadow: 0 0 20px rgba(255, 255, 255, 0.16);
}

:root[data-theme="light"] .scrub-word.lit {
  color: var(--ink);
  opacity: 1;
  text-shadow: none;
}

.scrub-word.dim {
  color: var(--subtle);
}

.scrub-word.dim.lit {
  color: var(--muted);
  opacity: 0.88;
}

@media (max-width: 52rem) {
  .manifesto { grid-template-columns: 1fr; gap: 1.5rem; }
  .manifesto-index { padding-top: 0; }
}

/* ── Services ──────────────────────────────────────────────────────────── */
/* An index and a stage. The four services were rows whose photographs only
   existed for a mouse: a popover followed the cursor, so a phone, a keyboard
   and a screen reader never saw them. Now the photographs are part of the
   content. On a phone, and before JavaScript, every service is a card with
   its image. On a wide screen site.js turns the same markup into proper ARIA
   tabs: the index on the left, the chosen service on a stage on the right,
   selected by hover, click, focus or the arrow keys. Each stage ends in a
   call to action that arrives at the enquiry form with that service already
   chosen. */

.svc-head { max-width: 46rem; margin: 0 0 clamp(2.5rem, 5vw, 3.75rem); }
.svc-head .lede { margin-top: 1.1rem; max-width: 38rem; }
.svc-head .lede a { color: var(--ink); text-underline-offset: 3px; }

/* Stacked: every service a card, image first. */
.svc-panels { display: grid; gap: clamp(2.75rem, 7vw, 4rem); }
.svc-panel { display: grid; gap: 1.35rem; align-content: start; }

.svc-media {
  margin: 0; overflow: hidden;
  aspect-ratio: 16 / 10;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--surface-2);
}
.svc-media img { display: block; width: 100%; height: 100%; object-fit: cover; }

.svc-body { display: grid; gap: 0.65rem; align-content: start; justify-items: start; }
.svc-index { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.12em; color: var(--subtle); font-variant-numeric: tabular-nums; }
.svc-title { font-size: clamp(1.35rem, 2.2vw, 1.75rem); font-weight: 500; letter-spacing: -0.03em; line-height: 1.2; }
.svc-desc { font-size: 0.95rem; line-height: 1.65; color: var(--muted); margin: 0; max-width: 36rem; }

.svc-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0.2rem 0 0.35rem; }
.svc-tags span:not(.tag-dot) { font-size: 0.75rem; color: var(--subtle); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 0.15rem 0.5rem; white-space: nowrap; }

.tag-dot {
  display: inline-block;
  width: 0.35rem; height: 0.35rem; border-radius: 50%;
  background: var(--verdigris); opacity: 0.4;
  margin-right: 0.25rem;
  transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.svc-panel:hover .tag-dot, .svc-panel.on .tag-dot { opacity: 1; transform: scale(1.25); }

.svc-cta { margin-top: 0.35rem; }

/* The index. Hidden until site.js shows it, and only ever on a wide screen. */
.svc-tabs { display: grid; align-content: start; border-top: 1px solid var(--line); }
.svc-tabs[hidden] { display: none; }

.svc-tab {
  display: grid; gap: 0.4rem; width: 100%;
  padding: 1.45rem 0.25rem;
  border: 0; border-bottom: 1px solid var(--line);
  background: none; color: inherit; font: inherit; text-align: left;
  cursor: pointer;
}
.svc-tab-index {
  font-size: 0.75rem; font-weight: 500; letter-spacing: 0.12em;
  color: var(--subtle); font-variant-numeric: tabular-nums;
  transition: color var(--t-fast) var(--ease);
}
.svc-tab-title {
  display: flex; align-items: center; gap: 0.9rem;
  font-size: clamp(1.4rem, 2.3vw, 2rem); font-weight: 500;
  letter-spacing: -0.03em; line-height: 1.15;
  color: var(--subtle);
  transition: color var(--t-mid) var(--ease), translate var(--t-mid) var(--ease);
}
/* The rubric draws itself beside the chosen service - the same 2px mark as
   every eyebrow, scaled rather than widened so it never relayouts. */
.svc-tab-title::after {
  content: ""; flex: none;
  width: 2.25rem; height: 2px; background: var(--rubric);
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--t-mid) var(--ease);
}
.svc-tab:hover .svc-tab-title,
.svc-tab:focus-visible .svc-tab-title { color: var(--muted); }
.svc-tab[aria-selected="true"] .svc-tab-title { color: var(--ink); }
.svc-tab[aria-selected="true"] .svc-tab-index { color: var(--rubric); }
.svc-tab[aria-selected="true"] .svc-tab-title::after { transform: scaleX(1); }
.svc-tab:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; border-radius: var(--r-sm); }

/* Wide, with JavaScript: index left, one service on the stage. */
.svc-explorer.tabbed {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: stretch;
}

/* The index runs the full height of the stage beside it, four equal rows,
   so the column reads as a table of contents for the picture rather than a
   short list with a void beneath it. */
.svc-explorer.tabbed .svc-tabs { grid-auto-rows: 1fr; }
.svc-explorer.tabbed .svc-tab { align-content: center; }
.svc-explorer.tabbed .svc-panels { align-self: start; }
.svc-explorer.tabbed .svc-panels { gap: 0; }
.svc-explorer.tabbed .svc-panel {
  grid-area: 1 / 1;
  visibility: hidden; opacity: 0;
  transition: opacity var(--t-mid) var(--ease), visibility 0s linear var(--t-mid);
}
.svc-explorer.tabbed .svc-panel.on {
  visibility: visible; opacity: 1;
  transition: opacity var(--t-mid) var(--ease), visibility 0s;
}
/* The tab already names the service, so the stage leads with the picture. */
.svc-explorer.tabbed .svc-panel .svc-index,
.svc-explorer.tabbed .svc-panel .svc-title { display: none; }

/* The stage change follows the index. Choosing a service further down the
   list wipes the new picture up out of the bottom of its frame while the old
   one leaves through the top; choosing one further up runs the other way, so
   the stage moves the way the eye moved. site.js sets data-dir and --dy, and
   parks the incoming panel on the far side first (.prep) so the wipe starts
   from the right edge. Clip-path, transform and opacity only. */
.svc-media { position: relative; }

.svc-explorer.tabbed .svc-media { clip-path: inset(100% 0 0 0); transition: clip-path 0.8s var(--ease); }
.svc-explorer.tabbed[data-dir="down"] .svc-panel:not(.on) .svc-media { clip-path: inset(0 0 100% 0); }
.svc-explorer.tabbed[data-dir="up"] .svc-panel:not(.on) .svc-media { clip-path: inset(100% 0 0 0); }
.svc-explorer.tabbed .svc-panel.on .svc-media { clip-path: inset(0); }

.svc-explorer.tabbed .svc-media img {
  transform: translateY(calc(var(--dy, 1) * -6%)) scale(1.12);
  transition: transform 1.2s var(--ease), translate 0.9s var(--ease);
}

/* On stage, the picture sits slightly large in its frame and shifts against
   the pointer, a few pixels, so the frame reads as a window onto it. */
.svc-explorer.tabbed .svc-panel.on .svc-media img {
  transform: scale(1.05);
  translate: calc(var(--px, 0) * -14px) calc(var(--py, 0) * -10px);
}

.svc-explorer.tabbed[data-dir="down"] .svc-panel.prep .svc-media { clip-path: inset(100% 0 0 0); transition: none; }
.svc-explorer.tabbed[data-dir="up"] .svc-panel.prep .svc-media { clip-path: inset(0 0 100% 0); transition: none; }
.svc-explorer.tabbed .svc-panel.prep .svc-media img { transform: translateY(calc(var(--dy, 1) * 6%)) scale(1.12); transition: none; }

.svc-explorer.tabbed .svc-body > * {
  opacity: 0;
  transform: translateY(calc(var(--dy, 1) * 12px));
  transition: opacity var(--t-mid) var(--ease), transform var(--t-mid) var(--ease);
}
.svc-explorer.tabbed .svc-panel.on .svc-body > * { opacity: 1; transform: none; }
.svc-explorer.tabbed .svc-panel.on .svc-body > :nth-child(3) { transition-delay: 60ms; }
.svc-explorer.tabbed .svc-panel.on .svc-body > :nth-child(4) { transition-delay: 120ms; }
.svc-explorer.tabbed .svc-panel.on .svc-body > :nth-child(5) { transition-delay: 180ms; }

/* The stage names what it shows, since the tab beside it carries the title:
   a glass caption with the service's place in the four. Only on the stage. */
.svc-caption { display: none; }

.svc-explorer.tabbed .svc-caption {
  position: absolute;
  left: 1rem;
  bottom: 1rem;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.45rem 0.95rem;
  border: 1px solid var(--glass-line);
  border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink);
  opacity: 0;
  translate: 0 0.5rem;
  transition: opacity 0.5s var(--ease), translate 0.6s var(--ease);
}

.svc-explorer.tabbed .svc-panel.on .svc-caption { opacity: 1; translate: none; transition-delay: 0.35s; }
.svc-caption-count { font-size: 0.75rem; font-variant-numeric: tabular-nums; letter-spacing: 0.06em; color: var(--muted); }

/* The index answers the pointer: a soft ground sweeps in behind a row, and
   the chosen row's title steps in a little, toward the stage it controls. */
.svc-tab { position: relative; isolation: isolate; }

.svc-tab::before {
  content: "";
  position: absolute;
  inset: 0.35rem -0.75rem;
  z-index: -1;
  border-radius: var(--r-md);
  background: var(--surface-1);
  opacity: 0;
  transform: scaleX(0.94);
  transition: opacity var(--t-mid) var(--ease), transform var(--t-mid) var(--ease);
}

.svc-tab:hover::before,
.svc-tab[aria-selected="true"]::before { opacity: 1; transform: none; }

.svc-tab[aria-selected="true"] .svc-tab-title { translate: 0.4rem 0; }

@media (prefers-reduced-motion: reduce) {
  .svc-explorer.tabbed .svc-media { clip-path: none; transition: none; }
  .svc-explorer.tabbed .svc-media img,
  .svc-explorer.tabbed .svc-body > *,
  .svc-explorer.tabbed .svc-panel { transition: none; transform: none; translate: none; }
  .svc-tab[aria-selected="true"] .svc-tab-title { translate: none; }
}

/* ── Work and process live below, after the shared pieces. */

/* ── Pricing ─────────────────────────────────────────────────────────────── */

/* Three subscription tiers. The prices carry a setup fee and a monthly figure,
   in two currencies, and the whole thing has to be readable before a single
   line of JavaScript runs - a prospect who sees no price is a prospect who
   leaves.

   So both currencies are in the markup and CSS chooses between them. The
   switch is hidden until .js is on the root, euros show by default, and the
   pound figures are simply the ones display:none is hiding. Nothing is
   rewritten at runtime and nothing is stored: privacy.html promises no cookies
   and no tracking, and a remembered currency preference would be a storage
   disclosure for the sake of one click.

   Each tier also states its first-year cost, setup included. It is simple
   arithmetic on the published figures (setup plus twelve months, the minimum
   term) and it is the number a business owner budgets with.

   Pricing sits in a room of its own, a panel a shade up from the page; see
   "From the promises into the prices" for how it arrives. */

#pricing { position: relative; isolation: isolate; }

#pricing::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-top: 1px solid var(--line);
  background: linear-gradient(180deg, var(--surface-2), var(--surface-1) 22rem);
  pointer-events: none;
}

/* The plan finder. One question, three answers taken from the tiers' own
   "for" lines. An answer lifts the matching tier, quiets the other two,
   says which it is in words for a screen reader, and ticks that plan on the
   enquiry form. Pressing it again undoes it. Without JavaScript it is not
   shown, since it would do nothing. */
.plan-finder { display: grid; justify-items: center; gap: 0.75rem; margin: 0 auto 1.5rem; text-align: center; }
.plan-finder[hidden] { display: none; }
.plan-finder-q { margin: 0; font-size: 0.875rem; color: var(--muted); }
.plan-finder-opts { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.4rem; }

.plan-finder-opts button {
  min-height: 2.5rem;
  padding: 0 1.05rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: transparent;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.plan-finder-opts button:hover { color: var(--ink); border-color: var(--line-hover); }
.plan-finder-opts button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.plan-finder-opts button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.plan-finder-result { min-height: 1.4em; margin: 0; font-size: 0.8125rem; color: var(--subtle); max-width: 40rem; }

.cur-row { display: flex; justify-content: center; }
.cur-switch { display: none; }

.js .cur-switch {
  position: relative;
  display: inline-grid;
  grid-template-columns: 1fr 1fr;
  margin: 0 auto clamp(2.5rem, 5vw, 3.5rem);
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface-1);
}

/* The thumb slides between the two; the labels only change colour over it. */
.js .cur-switch::before {
  content: "";
  position: absolute;
  top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 3px);
  border-radius: var(--r-pill);
  background: var(--ink);
  transition: translate var(--t-mid) var(--ease);
}

.cur-gbp .cur-switch::before { translate: 100% 0; }

.cur-switch button {
  position: relative;
  z-index: 1;
  min-height: 2.5rem;
  padding: 0 1.1rem;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--subtle);
  cursor: pointer;
  transition: color var(--t-mid) var(--ease);
}

.cur-switch button:hover { color: var(--ink); }
.cur-switch button[aria-pressed="true"] { color: var(--on-ink); }
.cur-switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.gbp { display: none; }
.cur-gbp .eur { display: none; }
.cur-gbp .gbp { display: inline; }

/* A price arrives rather than blinks when the currency changes: an element
   starts its animation afresh each time it starts being displayed. */
.tier-price .eur { display: inline-block; }
.cur-gbp .tier-price .eur { display: none; }
.cur-gbp .tier-price .gbp { display: inline-block; }

@media (prefers-reduced-motion: no-preference) {
  .js .tier-price .eur,
  .js .tier-price .gbp { animation: figure-in 420ms var(--ease) both; }
}

@keyframes figure-in { from { opacity: 0; transform: translateY(0.3em); } }

.tier-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.7rem; }

.tier {
  display: flex;
  flex-direction: column;
  padding: 1.75rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--t-mid) var(--ease);
}

.tier:hover { border-color: var(--line-strong); }

/* The pointer's light, as on the bento tiles. */
.tier { position: relative; isolation: isolate; --sx: 50%; --sy: 0%; }

.tier::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(26rem circle at var(--sx) var(--sy), var(--accent-soft), transparent 62%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-mid) var(--ease);
}

.tier:hover::before { opacity: 1; }

/* An answer from the plan finder. */
.tier-fit {
  display: none;
  align-self: flex-start;
  align-items: center;
  gap: 0.4rem;
  margin: 0.15rem 0 0.9rem;
  padding: 0.25rem 0.7rem;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--ink);
}

.tier.fit .tier-fit { display: inline-flex; }

.tier-grid .tier { transition: border-color var(--t-mid) var(--ease), box-shadow var(--t-mid) var(--ease), opacity 0.5s var(--ease), scale 0.5s var(--ease); }
/* Qualified as far as the scroll-driven arrival below, which sets opacity
   on every tier and would otherwise win. */
.tier-grid[data-fit] .tier:not(.fit),
.js .tier-grid[data-fit] > .tier[data-reveal]:not(.fit) { opacity: 0.45; scale: 0.975; }
.tier.fit { box-shadow: 0 0 0 1px var(--accent), 0 34px 70px -34px var(--accent-line); }

.tier-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.25rem 1rem; margin-bottom: 0.35rem; }
.tier h3 { font-size: 1.05rem; font-weight: 500; }
.tier-flag { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); white-space: nowrap; }
.tier-for { font-size: 0.8125rem; color: var(--subtle); margin: 0 0 1.4rem; max-width: none; }

.tier-price { margin: 0 0 0.25rem; font-size: 1.6rem; font-weight: 500; letter-spacing: -0.035em; color: var(--ink); }
.tier-price small { font-size: 0.8125rem; font-weight: 500; letter-spacing: 0; color: var(--subtle); }
.tier-setup { margin: 0 0 0.9rem; font-size: 0.8125rem; color: var(--subtle); max-width: none; }

.tier-year {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.2rem 1rem;
  margin: 0 0 1.4rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: 0.75rem;
  color: var(--subtle);
  max-width: none;
}

.tier-year strong { font-size: 0.875rem; font-weight: 500; color: var(--ink); font-variant-numeric: tabular-nums; }

.tier-includes {
  list-style: none; margin: 0; padding: 1.15rem 0 0;
  border-top: 1px solid var(--line);
  font-size: 0.8125rem; color: var(--subtle); line-height: 1.5;
  display: grid; gap: 0.55rem;
}
.tier-includes strong { display: block; font-weight: 500; color: var(--ink); }

/* The middle tier is the one to take, and the page should say so once, in the
   same cobalt the mark uses - not with a shadow and a scale transform. */
.tier.featured { background: var(--accent); border-color: transparent; }
.tier.featured h3, .tier.featured .tier-price, .tier.featured .tier-includes strong { color: #fff; }
.tier.featured .tier-flag { color: rgba(255, 255, 255, 0.88); }
.tier.featured .tier-for,
.tier.featured .tier-price small,
.tier.featured .tier-setup,
.tier.featured .tier-year,
.tier.featured .tier-includes { color: rgba(255, 255, 255, 0.88); }
.tier.featured .tier-year { border-color: rgba(255, 255, 255, 0.3); }
.tier.featured .tier-year strong { color: #fff; }
.tier.featured .tier-includes { border-top-color: rgba(255, 255, 255, 0.26); }
.tier.featured:hover { border-color: transparent; }
.tier.featured .tier-fit { color: #fff; border-color: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.12); }
.tier.featured::before { background: radial-gradient(24rem circle at var(--sx) var(--sy), rgba(255, 255, 255, 0.16), transparent 60%); }

/* A sheen crosses the recommended plan once as it arrives, and again under
   the pointer. Background position only: paint, not layout. */
.tier.featured { overflow: hidden; }

.tier.featured::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, 0.18) 50%, transparent 62%) no-repeat;
  background-size: 250% 100%;
  background-position: 130% 0;
  pointer-events: none;
  transition: background-position 1.4s var(--ease);
}

.tier.featured:hover::after { background-position: -30% 0; }

@media (prefers-reduced-motion: no-preference) {
  .tier.featured.in::after { animation: sheen 1.6s var(--ease) 0.7s; }
}

@keyframes sheen { from { background-position: 130% 0; } to { background-position: -30% 0; } }

@media (max-width: 62rem) { .tier-grid { grid-template-columns: minmax(0, 1fr); } }

/* The terms, each with the clause it comes from. */
.price-notes { margin-top: clamp(2.5rem, 5vw, 3.5rem); }
.price-notes h3 { font-size: 0.95rem; font-weight: 500; margin-bottom: 1.2rem; }

.note-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 1.25rem 2rem; margin-bottom: 1.6rem; }
.note-grid > div { display: flex; flex-direction: column; padding-top: 1rem; border-top: 1px solid var(--line); }
.note-grid strong { display: block; font-weight: 500; font-size: 0.875rem; color: var(--ink); margin-bottom: 0.2rem; }
.note-grid span { font-size: 0.8125rem; color: var(--subtle); line-height: 1.55; }

.note-ref {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  min-height: 2.75rem;
  min-width: 2.5rem;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: 0.2em;
  transition: color var(--t-fast) var(--ease), text-decoration-color var(--t-fast) var(--ease);
}

.note-ref:hover { color: var(--ink); text-decoration-color: currentColor; }

@media (prefers-reduced-motion: reduce) {
  .js .cur-switch::before { transition: none; }
}

/* ── FAQ ─────────────────────────────────────────────────────────────────── */

/* The questions a business owner asks before signing, grouped the way they
   think about them - the service, ownership and leaving, working together -
   rather than as one undifferentiated list. The rail on the left holds the way
   to ask something that is not answered here: an FAQ that ends without a next
   step is a dead end, and this one sits two sections above the enquiry form.

   One answer open at a time, natively: <details name="faq"> is an exclusive
   accordion in every current browser and needs no script. Older browsers just
   let several open, which costs nothing.

   The numbers are a CSS counter rather than text, so the question read out of
   the markup by tools/jsonld.js is the question, not "04 Do I own..." */

.faq-layout {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2.5rem, 7vw, 6rem);
  align-items: start;
}

.faq-rail { position: sticky; top: 7rem; }
.faq-rail h2 { margin-bottom: 1.1rem; }
.faq-rail .lede { margin-bottom: 2.25rem; }

.faq-ask { padding-top: 1.5rem; border-top: 1px solid var(--line); }
.faq-ask p { font-size: 0.9rem; line-height: 1.6; color: var(--muted); margin-bottom: 1.1rem; max-width: 22rem; }

.faq-list { counter-reset: faq; }

.faq-group {
  font-size: 0.75rem; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--subtle); margin: 0 0 0.85rem;
}
.faq + .faq-group { margin-top: 2.75rem; }

.faq { border-top: 1px solid var(--line); }
.faq:has(+ .faq-group), .faq:last-child { border-bottom: 1px solid var(--line); }

.faq summary {
  counter-increment: faq;
  display: grid; grid-template-columns: 2.4rem minmax(0, 1fr) auto;
  align-items: center; gap: 1rem;
  padding: 1.25rem 0.25rem;
  cursor: pointer; list-style: none;
  font-size: 1.02rem; font-weight: 500; line-height: 1.4; color: var(--ink);
  transition: color var(--t-fast) var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }

.faq summary::before {
  content: counter(faq, decimal-leading-zero);
  font-size: 0.75rem; font-variant-numeric: tabular-nums;
  color: var(--subtle);
  transition: color var(--t-fast) var(--ease);
}

/* Plus that closes to a minus: two hairlines drawn as backgrounds, and the
   vertical one scaled away. Background size is paint, not layout. */
.faq summary::after {
  content: "";
  width: 1.9rem; height: 1.9rem; border-radius: 50%;
  border: 1px solid var(--line);
  color: var(--subtle);
  background:
    linear-gradient(currentColor, currentColor) center / 0.7rem 1px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1px 0.7rem no-repeat;
  transition: transform var(--t-mid) var(--ease),
              background-size var(--t-mid) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}

.faq summary:hover::after { border-color: var(--line-strong); color: var(--ink); }
.faq summary:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; border-radius: var(--r-sm); }

.faq[open] summary::before { color: var(--rubric); }
.faq[open] summary::after {
  background-size: 0.7rem 1px, 1px 0;
  border-color: var(--accent-line); color: var(--ink);
  transform: rotate(180deg);
}

/* The answer sits under the question text, not under the number, and arrives
   with a short fade. Opacity and transform only: animating the height of an
   opening details element is layout work on every frame. */
.faq > div { padding: 0 3rem 1.6rem 3.4rem; }
.faq[open] > div { animation: faq-in var(--t-mid) var(--ease); }
.faq p { font-size: 0.9375rem; line-height: 1.7; color: var(--muted); max-width: 40rem; }

@keyframes faq-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}

@media (max-width: 62rem) {
  .faq-layout { grid-template-columns: 1fr; gap: 2.5rem; }
  .faq-rail { position: static; }
}

@media (max-width: 34rem) {
  .faq summary { grid-template-columns: 1.9rem minmax(0, 1fr) auto; gap: 0.75rem; font-size: 0.98rem; }
  .faq > div { padding: 0 0.25rem 1.4rem 2.65rem; }
}

@media (prefers-reduced-motion: reduce) {
  .faq[open] > div { animation: none; }
  .faq[open] summary::after { transform: none; }
}

/* ── Founder ─────────────────────────────────────────────────────────────── */
/* The person a prospect would be working with, told as a career rather than
   a tag cloud. The stops are profile.html's own, in its order, newest first,
   and they must stay in step with it: AGENTS.md says a client will check.
   Each stop carries the tools actually used there, so "Go" reads as four
   years of production Go at Amadeus rather than a word in a list, and
   pointing at a stop lights its tools. The line down the left is the page's
   own motif; it draws in as the list arrives, and the current stop is lit in
   verdigris. The portrait holds its place beside the career on a wide
   screen, as a signature does beside a letter. */

.founder-grid {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 7fr);
  gap: clamp(2.5rem, 6vw, 5.5rem);
  align-items: start;
}

.founder-portrait { position: sticky; top: 6.5rem; margin: 0; }

.founder-portrait img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 15%;
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.founder-portrait figcaption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
}

.founder-portrait strong { font-size: 0.9375rem; font-weight: 500; color: var(--ink); }
.founder-portrait figcaption span { font-size: 0.75rem; color: var(--subtle); }

.founder-body h2 { margin-bottom: 1.15rem; text-wrap: balance; }
.founder-body .lede { max-width: 38rem; margin-bottom: 2.25rem; }

.career {
  --dot: 0.625rem;
  position: relative;
  margin: 0 0 2.25rem;
  padding: 0 0 0 1.75rem;
  list-style: none;
}

.career::before {
  content: "";
  position: absolute;
  top: 0.55rem;
  bottom: 1.6rem;
  left: calc(var(--dot) / 2 - 0.5px);
  width: 1px;
  background: var(--line-strong);
  transform-origin: center top;
}

.career-step {
  position: relative;
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: 0.6rem 1.5rem;
  padding: 1.15rem 0;
  border-bottom: 1px solid var(--line);
}

.career-step:first-child { padding-top: 0; }

.career-step::before {
  content: "";
  position: absolute;
  top: 1.45rem;
  left: -1.75rem;
  width: var(--dot);
  height: var(--dot);
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--paper);
  transition: border-color var(--t-mid) var(--ease), background var(--t-mid) var(--ease);
}

.career-step:first-child::before { top: 0.3rem; }
.career-step.now::before { background: var(--verdigris); border-color: var(--verdigris); }
.career-step:hover::before { border-color: var(--accent); }

.career-when { grid-row: span 2; padding-top: 0.2rem; font-size: 0.75rem; letter-spacing: 0.06em; color: var(--subtle); font-variant-numeric: tabular-nums; }
.career-who strong { display: block; font-size: 1.05rem; font-weight: 500; color: var(--ink); }
.career-who span { display: block; margin-top: 0.15rem; font-size: 0.8125rem; color: var(--muted); }

.career-stack { grid-column: 2; display: flex; flex-wrap: wrap; gap: 0.35rem; }

.career-stack span {
  padding: 0.18rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: 0.75rem;
  color: var(--subtle);
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}

.career-step:hover .career-stack span { color: var(--ink); border-color: var(--line-strong); }

/* The line draws in, then the stops arrive down it, newest first. */
@media (prefers-reduced-motion: no-preference) {
  .js .career::before { transform: scaleY(0); transition: transform 900ms var(--ease) 150ms; }
  .js .career.in::before { transform: none; }

  .js .career-step { opacity: 0; translate: 0 10px; transition: opacity 600ms var(--ease), translate 600ms var(--ease); }
  .js .career.in .career-step { opacity: 1; translate: none; }
  .js .career.in .career-step:nth-child(1) { transition-delay: 250ms; }
  .js .career.in .career-step:nth-child(2) { transition-delay: 400ms; }
  .js .career.in .career-step:nth-child(3) { transition-delay: 550ms; }
}

@media (max-width: 52rem) {
  .founder-grid { grid-template-columns: minmax(0, 1fr); }
  .founder-portrait { position: static; max-width: 20rem; }
}

@media (max-width: 30rem) {
  .career-step { grid-template-columns: minmax(0, 1fr); gap: 0.4rem; }
  .career-when { grid-row: auto; padding-top: 0; }
  .career-stack { grid-column: 1; }
}

/* Tool tags, as the profile's CV rows still use them. */
.tagrow { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 1.3rem 0; }
.tagrow span { font-size: 0.75rem; color: var(--subtle); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 0.18rem 0.55rem; }

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

footer { padding: clamp(4rem, 8vw, 6rem) 0 0; border-top: 1px solid var(--line); font-size: 0.85rem; color: var(--subtle); overflow: clip; }

.footer-top { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 2.5rem; margin-bottom: 3rem; }
.footer-brand { display: block; margin-bottom: 0.35rem; font-size: 0.95rem; font-weight: 500; color: var(--ink); }
.footer-tag { margin: 0; font-size: 0.85rem; color: var(--subtle); max-width: 22rem; }

.footer-links { display: flex; flex-wrap: wrap; gap: 0 1.6rem; max-width: 36rem; }
.footer-links a { display: inline-flex; align-items: center; min-height: 2.75rem; min-width: 2.5rem; padding: 0 0.35rem; margin: 0 -0.35rem; color: var(--subtle); text-decoration: none; }
.footer-links a:hover { color: var(--ink); }

.footer-legal { padding-top: 1.5rem; border-top: 1px solid var(--line); font-size: 0.75rem; color: var(--subtle); }

/* On the home page the legal lines share the rule with the clock and the way
   back up. The other pages keep their single column. */
.footer-legal:has(.footer-meta) { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem 2.5rem; }

.footer-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.75rem; }
.footer-clock { font-size: 0.75rem; letter-spacing: 0.04em; color: var(--subtle); }
.footer-clock[hidden] { display: none; }

.to-top {
  display: inline-flex; align-items: center; gap: 0.5rem;
  min-height: 2.75rem;
  font-size: 0.75rem; font-weight: 500;
  color: var(--muted); text-decoration: none;
}
.to-top:hover { color: var(--ink); }

/* The way back up sits in a ring that fills as the page is read, tied to
   the document's scroll by the browser. A hairline track shows the whole
   circle; the cobalt arc is how far down the reader is. */
.to-top-ring {
  position: relative;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--line-strong);
  color: var(--ink);
}

.to-top-ring > svg:not(.ring) { width: 0.8rem; height: 0.8rem; transition: transform var(--t-mid) var(--ease); }
.to-top:hover .to-top-ring > svg:not(.ring) { transform: translateY(-3px); }

.ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.ring circle { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linecap: round; stroke-dasharray: 100; stroke-dashoffset: 0; }

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .ring circle { animation: ring-fill linear both; animation-timeline: scroll(root); }
  }
}

@keyframes ring-fill { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }

/* ── The home footer ─────────────────────────────────────────────────────── */
/* The last screen of the page, arranged as a colophon: who the studio is
   and how to write to it on the left, the site in three labelled columns on
   the right, the legal lines and the way back up beneath, and the name set
   across the foot. Links answer the pointer with an underline drawn from
   the left. The other pages keep their shorter footer. */
.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2.5rem, 6vw, 5rem);
  margin-bottom: clamp(3rem, 6vw, 4.5rem);
}

.footer-intro .footer-brand { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.6rem; font-size: 1.05rem; }
.footer-intro .footer-tag { max-width: 24rem; }

.footer-mail {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  max-width: 100%;
  min-height: 2.75rem;
  margin: 1.5rem 0 1.25rem;
  font-size: clamp(1.15rem, 2.1vw, 1.55rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
  background: linear-gradient(currentColor, currentColor) no-repeat 0 92% / 0 1px;
  transition: background-size 0.55s var(--ease);
}

.footer-mail:hover { color: var(--ink); background-size: 100% 1px; }
.footer-mail svg { flex: none; width: 0.7em; height: 0.7em; transition: transform var(--t-mid) var(--ease); }
.footer-mail:hover svg { transform: translate(3px, -3px); }

.footer-nav { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem 2rem; }
.footer-col { min-width: 0; }
.footer-col-head { margin: 0 0 0.75rem; font-size: 0.75rem; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--subtle); }
.footer-col ul { display: grid; margin: 0; padding: 0; list-style: none; }

.footer-col a {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 2.5rem;
  min-width: 2.5rem;
  font-size: 0.9375rem;
  color: var(--muted);
  text-decoration: none;
  background: linear-gradient(currentColor, currentColor) no-repeat 0 72% / 0 1px;
  transition: color var(--t-fast) var(--ease), background-size 0.45s var(--ease);
}

.footer-col a:hover { color: var(--ink); background-size: 100% 1px; }
.footer-col a svg { width: 0.6rem; height: 0.6rem; }

@media (max-width: 62rem) {
  .footer-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 30rem) {
  .footer-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (prefers-reduced-motion: reduce) {
  .footer-mail, .footer-col a { transition: none; }
}
.legal-line { margin: 0 0 0.35rem; max-width: none; color: var(--subtle); }
.legal-line:last-child { margin-bottom: 0; }

/* The signature. Architectural, cropped against the bottom edge. */
.wordmark {
  display: block; width: 100%;
  margin: clamp(2.5rem, 6vw, 4rem) 0 -0.16em;
  font-size: clamp(4.5rem, 17vw, 17.5rem);
  font-weight: 500; letter-spacing: -0.06em; line-height: 0.78;
  text-align: center; color: var(--ink);
  opacity: 0.055; user-select: none; white-space: nowrap;
}

/* ── Legal and sub-pages ─────────────────────────────────────────────────── */

.up { display: inline-flex; align-items: center; min-height: 2.75rem; gap: 0.4rem; margin-bottom: 1.75rem; font-size: 0.85rem; text-decoration: none; color: var(--subtle); }
.up:hover { color: var(--ink); }

.legal { padding-top: clamp(6rem, 11vw, 9rem); padding-bottom: clamp(4rem, 8vw, 6rem); }
.legal h1 { font-size: clamp(2.1rem, 4.5vw, 3.1rem); margin-bottom: 1.5rem; }
.legal .subtitle { margin: -0.75rem 0 2rem; font-size: 1.05rem; color: var(--subtle); max-width: 40rem; }
.casestudy .subtitle { font-size: 1.05rem; line-height: 1.6; color: var(--subtle); margin: -0.5rem 0 2rem; max-width: 40rem; }

.legal h2 { font-size: 1.1rem; font-weight: 500; letter-spacing: -0.015em; color: var(--ink); margin: 2.75rem 0 0.85rem; }

dl.record { margin: 1.75rem 0; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
dl.record > div { display: grid; grid-template-columns: 11rem 1fr; gap: 0 1rem; padding: 0.8rem 1.2rem; border-bottom: 1px solid var(--line); }
dl.record > div:last-child { border-bottom: 0; }
dl.record dt { margin: 0; font-weight: 500; font-size: 0.85rem; color: var(--ink); }
dl.record dd { margin: 0; color: var(--subtle); font-size: 0.85rem; }

@media (max-width: 34rem) { dl.record > div { grid-template-columns: 1fr; gap: 0.2rem; } }

.updated { margin-top: 2.5rem; font-size: 0.78rem; color: var(--subtle); }
.pending { color: var(--pending); white-space: nowrap; }

.callout { margin: 1.75rem 0 2.5rem; padding: 1.1rem 1.25rem; border: 1px solid var(--pending); border-radius: var(--r-sm); background: var(--pending-soft); }
.callout p { margin: 0; font-size: 0.9rem; max-width: none; color: var(--muted); }
.callout.neutral { border-color: var(--accent-line); background: var(--accent-soft); }

.project-record { margin: 0; display: grid; }
.project-record > div { display: grid; grid-template-columns: 8rem 1fr; gap: 0 1rem; padding: 0.7rem 0; border-bottom: 1px solid var(--line); }
.project-record > div:last-child { border-bottom: 0; }
.project-record dt { margin: 0; font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); padding-top: 0.15rem; }
.project-record dd { margin: 0; font-size: 0.9rem; color: var(--ink); }
.project-record.wide { border: 1px solid var(--line); border-radius: var(--r-md); padding: 0.25rem 1.2rem; margin: 1.75rem 0; }
.project-record.wide > div { grid-template-columns: 9rem 1fr; }

@media (max-width: 30rem) { .project-record > div, .project-record.wide > div { grid-template-columns: 1fr; gap: 0.15rem; } }

.pill { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.78rem; padding: 0.15rem 0.55rem; border-radius: var(--r-sm); border: 1px solid transparent; }
.pill::before { content: ""; width: 0.375rem; height: 0.375rem; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.pill.live { color: var(--success); background: var(--success-soft); border-color: var(--success-line); }

/* ── Profile ─────────────────────────────────────────────────────────────── */
/* The page Eddy sends to a prospect who has already decided to look him up, so
   it is built as a document rather than a pitch: a masthead, a ledger of what
   he has actually done, and no second copy of the home page's argument.

   Headings are deliberately smaller here than anywhere else on the site. The
   global h2 is display scale, sized for one statement per screen; a profile has
   six sections in a row and at that size every one of them shouted over the
   name at the top of the page. The eyebrow does the labelling instead. */

.profile h1 { font-size: clamp(2.4rem, 4.6vw, 3.6rem); margin-bottom: 0.55rem; }
.profile h2 { font-size: clamp(1.45rem, 2.3vw, 2rem); letter-spacing: -0.03em; }

/* Closer than the home page, and ruled. The home page's band spacing is built
   for one idea per screen; six short sections at that rhythm left the page
   mostly empty and the reader scrolling through nothing. A hairline at each
   section start does the separating that the whitespace was failing to do,
   and it reads as a document rather than a deck. */
.profile .band { padding: clamp(3.25rem, 6vw, 5rem) 0; border-top: 1px solid var(--line); }
.profile .section-head { padding: clamp(4.5rem, 8vw, 6.5rem) 0 clamp(3rem, 6vw, 4.5rem); }

.prof-masthead { display: grid; grid-template-columns: 16rem 1fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: end; }

/* The portrait is a cut-out with a real alpha channel, so it is stood on a
   ground and bottom-aligned rather than cropped into a circle: the subject
   breaks the frame the way a printed portrait does, and nothing of the head is
   lost to a mask. object-fit is contain for the same reason - cover would crop
   a square source to fill this taller frame.

   The ground is light, and that is the whole reason it works. On any of the
   dark surfaces a man in a navy suit simply disappeared - measured, not
   guessed: the subject's darkest values sit within a few points of
   --surface-1. Reading it as a print laid on the page solves the contrast and
   is the more considered answer than lightening the photograph. */
.prof-portrait {
  position: relative; margin: 0; overflow: hidden;
  background: linear-gradient(178deg, #ececea 0%, #d9d9d5 100%);
  border-radius: var(--r-md);
}
.prof-portrait img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: contain; object-position: bottom center; padding-top: 1.5rem; }

.prof-role { font-size: clamp(1rem, 1.3vw, 1.1rem); color: var(--ink); margin: 0 0 0.3rem; max-width: none; }
.prof-place { font-size: 0.875rem; color: var(--subtle); margin: 0; max-width: none; }
.prof-id .lede { margin-top: 1.5rem; max-width: 36rem; }
.prof-id .cta-row { margin-top: 1.9rem; }

/* Four facts a prospect checks before reading a word of the prose. */
.fact-rail { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); margin: clamp(3rem, 6vw, 4.5rem) 0 0; border-top: 1px solid var(--line); }
.fact-rail > div { padding: 1.3rem 1.5rem 1.3rem 0; border-bottom: 1px solid var(--line); }
.fact-rail dt { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--subtle); margin-bottom: 0.45rem; }
.fact-rail dd { margin: 0; font-size: 0.9rem; line-height: 1.5; color: var(--ink); }

/* Label rail beside the prose. Sticky is safe: .shell clips with overflow-x
   clip rather than hidden precisely so it does not become a scroll container. */
.prof-split { display: grid; grid-template-columns: 13rem 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
.prof-rail { position: sticky; top: 7rem; }
.prof-rail .eyebrow { margin-bottom: 1rem; }
.prof-body p { font-size: 1rem; line-height: 1.78; max-width: 40rem; }

/* The career, as a ledger. Rows, hairlines, tabular years - not four boxes. */
.cv-list { border-top: 1px solid var(--line); }
.cv-row {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1.05fr) minmax(0, 1.15fr);
  gap: clamp(1rem, 3vw, 2.5rem);
  align-items: start;
  padding: clamp(1.5rem, 2.6vw, 2.1rem) clamp(0.5rem, 1.5vw, 1.25rem);
  border-bottom: 1px solid var(--line);
  transition: background var(--t-mid) var(--ease);
}
.cv-row:hover { background: var(--surface-2); }

.cv-when { font-size: 0.75rem; color: var(--subtle); font-variant-numeric: tabular-nums; padding-top: 0.35rem; }
.cv-org { display: block; font-size: clamp(1.1rem, 1.7vw, 1.35rem); font-weight: 500; letter-spacing: -0.025em; color: var(--ink); }
.cv-org a { display: inline-flex; align-items: center; min-height: 2.75rem; text-decoration: none; }
.cv-org a:hover { color: var(--muted); }
.cv-what { display: block; font-size: 0.8125rem; color: var(--subtle); margin-top: 0.35rem; }
.cv-note { font-size: 0.875rem; line-height: 1.65; color: var(--muted); margin: 0; max-width: none; }
.cv-row .tagrow { margin: 1rem 0 0; }

/* Current work is marked with the rubric rather than a coloured pill: the two
   own ventures are running now, the two employed roles are history. */
.cv-row.now .cv-when::before {
  content: ""; display: inline-block; width: 18px; height: 2px;
  background: var(--rubric); vertical-align: middle; margin-right: 0.5rem;
}

/* Four groups, four columns. auto-fit was giving three and then stranding
   Commerce alone on a second row. */
.stack-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.9rem 2rem; }
@media (max-width: 72rem) { .stack-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 34rem) { .stack-grid { grid-template-columns: 1fr; } }
.stack-grid h3 { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--subtle); }
.stack-grid .tagrow { margin: 0.85rem 0 0; }

.principles { display: grid; grid-template-columns: 1fr 1fr; gap: 0 clamp(2rem, 5vw, 4rem); border-top: 1px solid var(--line); }
.principle { display: grid; grid-template-columns: 2.6rem 1fr; padding: 1.7rem 0; border-bottom: 1px solid var(--line); }
.principle-num { font-size: 0.75rem; color: var(--subtle); font-variant-numeric: tabular-nums; padding-top: 0.28rem; }
.principle h3 { font-size: 1rem; font-weight: 500; margin-bottom: 0.4rem; }
.principle p { font-size: 0.875rem; line-height: 1.65; color: var(--muted); margin: 0; max-width: none; }

/* The one heading on this page allowed back up towards display scale. It is
   the last thing before the footer and the only place asking for an action. */
.profile .prof-close h2 { font-size: clamp(1.9rem, 3.1vw, 2.75rem); }
.prof-close .lede { margin-top: 1.1rem; max-width: 36rem; }
.prof-close .fineprint { margin-top: 1.75rem; max-width: 40rem; }

@media (max-width: 62rem) {
  .prof-masthead { grid-template-columns: 12rem 1fr; gap: 2rem; }
  .prof-split { grid-template-columns: 1fr; gap: 1.75rem; }
  .prof-rail { position: static; }
  .cv-row { grid-template-columns: 1fr; gap: 0.75rem; }
  .cv-when { padding-top: 0; }
  .principles { grid-template-columns: 1fr; }
}

@media (max-width: 34rem) {
  .prof-masthead { grid-template-columns: 1fr; }
  .prof-portrait { max-width: 13rem; }
}

.casestudy h3 { font-size: 1.05rem; font-weight: 500; margin: 1.9rem 0 0.6rem; }
.casestudy .visit { margin: 2rem 0 1rem; }

/* The 404 and the thank-you page share a masthead, but only the 404 may carry
   .notfound: site.js rewrites that page into French under /fr/. */
.notfound .errcode, .casestudy .errcode, .thanks .errcode {
  font-size: 0.75rem; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--subtle); margin-bottom: 0.9rem;
}

.notfound h1, .thanks h1 { font-size: clamp(2.1rem, 4.5vw, 3.1rem); margin-bottom: 1.25rem; }

.plainlist { list-style: none; margin: 0; padding: 0; max-width: var(--measure); }
.plainlist li { padding: 0.85rem 0; border-top: 1px solid var(--line); font-size: 0.9rem; color: var(--subtle); }
.plainlist li:first-child { border-top: 0; }
.plainlist a { font-weight: 500; }

.fineprint { font-size: 0.8125rem; color: var(--subtle); margin-top: 1rem; line-height: 1.6; }
.strong-link { font-weight: 500; }

/* ── The horizon under Contact ───────────────────────────────────────────── */
/* The light the closing section used to carry, now under the contact form so
   the page ends where the ask is. Four layers rather than one gradient,
   because a single radial reads as a lit circle; light with an edge needs a
   bloom behind it, a bright core, haze around it and a thin rim on top. It
   sits in the lower third, beneath the form rather than behind its fields,
   and fades out before the section ends so it dissolves into the footer
   instead of stopping at a line. */

.cta-atmosphere {
  opacity: var(--atmos);
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 45%, #000 78%, transparent 99%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 45%, #000 78%, transparent 99%);
}

.cta-atmosphere > * {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 50%;
}

.cta-bloom {
  top: 70%;
  width: 150%;
  height: 21rem;
  background: radial-gradient(closest-side, rgba(23, 54, 201, 0.42) 0%, rgba(23, 54, 201, 0.1) 54%, transparent 80%);
  filter: blur(120px);
}

.cta-horizon {
  top: 86%;
  width: 88%;
  height: 7.5rem;
  background: radial-gradient(closest-side, rgba(48, 93, 255, 0.9) 0%, rgba(32, 72, 255, 0.28) 44%, transparent 74%);
  filter: blur(58px);
}

.cta-haze {
  top: 80%;
  width: 172%;
  height: 15rem;
  background: radial-gradient(closest-side, rgba(32, 72, 255, 0.2) 0%, transparent 74%);
  filter: blur(105px);
}

.cta-edge {
  top: 85%;
  width: 54%;
  height: 2.6rem;
  background: radial-gradient(closest-side, rgba(120, 165, 255, 0.62) 0%, transparent 72%);
  filter: blur(26px);
}

/* The section dissolves into the footer instead of meeting it at a rule. */
main:has(> #contact:last-child) + footer { border-top: 0; }

@media (max-width: 48rem) {
  /* Wider and softer on a phone: the light should still be felt without
     taking over a screen that is mostly text. */
  .cta-bloom { width: 200%; height: 20rem; filter: blur(90px); }
  .cta-horizon { width: 140%; height: 9rem; filter: blur(56px); }
  .cta-haze { width: 190%; height: 16rem; filter: blur(80px); }
  .cta-edge { width: 100%; height: 3.5rem; filter: blur(34px); }
  .cta-atmosphere { opacity: calc(var(--atmos) * 0.82); }
}

/* ── Selected work ───────────────────────────────────────────────────────── */
/* Two projects, so they get room rather than a grid: editorial rows that
   alternate side. Each screenshot sits in a window whose address bar shows
   the real domain, and that address is the link. The one thing a portfolio
   can honestly offer, somewhere to go and check, is also the largest thing
   on it to press: the address stretches its hit area over the whole window,
   so there is one link, one tab stop and one accessible name. */

/* A deck. Each project is a card; on a screen tall enough to hold one, the
   cards stick in turn a little below the last, and each recedes - a shade
   smaller, a shade darker - as the next slides up over it. --n is a card's
   place in the deck and --cover how far the next has covered it, both set
   by site.js. The deck ends on the card that is not a project yet. */
.projects { display: grid; gap: clamp(1.25rem, 2.5vw, 2rem); }

.proj {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: clamp(2rem, 4.5vw, 4rem);
  align-items: center;
  padding: clamp(1.25rem, 2.6vw, 2.25rem);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  transform-origin: 50% 0;
}

/* The shade a covered card takes. */
.proj::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: 3;
  border-radius: inherit;
  background: var(--paper);
  opacity: calc(var(--cover, 0) * 0.6);
  pointer-events: none;
}

@media (min-width: 58.0625rem) and (min-height: 46rem) and (prefers-reduced-motion: no-preference) {
  .projects.deck .proj {
    position: sticky;
    top: calc(5.75rem + var(--n, 0) * 1.1rem);
    scale: calc(1 - var(--cover, 0) * 0.05);
  }
}

/* The empty place at the end of the deck: a window with nothing in it yet. */
.proj-next-body { display: grid; justify-items: start; gap: 0.9rem; }
.proj-next-body .proj-title,
.proj-next-body .proj-desc { margin: 0; }
.proj-next-body .pair { margin-top: 0.6rem; }

.proj-next-art {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  border: 1px dashed var(--line-hover);
  border-radius: var(--r-md);
  background: repeating-linear-gradient(135deg, transparent 0 14px, var(--line) 14px 15px);
}

.proj-next-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  gap: 0.4rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px dashed var(--line-strong);
}

.proj-next-bar span { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--line-strong); }

.proj-next-plus {
  width: 3.25rem;
  height: 3.25rem;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background:
    linear-gradient(var(--ink), var(--ink)) center / 1rem 1px no-repeat,
    linear-gradient(var(--ink), var(--ink)) center / 1px 1rem no-repeat,
    var(--surface-2);
  transition: transform var(--t-mid) var(--ease), border-color var(--t-mid) var(--ease);
}

.proj-next:hover .proj-next-plus { transform: rotate(90deg) scale(1.08); border-color: var(--accent); }

.proj:nth-child(even) .proj-window { order: 2; }

.proj-window {
  position: relative;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-1);
  box-shadow: 0 24px 60px -30px var(--shadow-strong);
  transition: border-color var(--t-mid) var(--ease), box-shadow var(--t-mid) var(--ease);
}

.proj-window:hover { border-color: var(--line-strong); box-shadow: 0 30px 70px -28px var(--shadow-strong); }
.proj-window:has(.proj-url:focus-visible) { outline: 2px solid var(--accent); outline-offset: 4px; }

.proj-bar {
  display: flex;
  justify-content: center;
  height: 2.75rem;
  padding: 0 0.75rem;
  border-bottom: 1px solid var(--line);
  border-radius: calc(var(--r-md) - 1px) calc(var(--r-md) - 1px) 0 0;
  background: var(--surface-1);
}

.proj-url {
  display: flex;
  align-items: center;
  flex: 0 1 24rem;
  min-width: 0;
  color: var(--muted);
  text-decoration: none;
}

/* Above the screenshot, which its settle animation lifts onto a layer of its own. */
.proj-url::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.proj-url:focus-visible { outline: none; }

.proj-address {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.35rem 0.9rem;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  font-size: 0.8125rem;
  line-height: 1.2;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}

.proj-address svg { width: 0.7rem; height: 0.7rem; flex: none; transition: transform var(--t-mid) var(--ease); }
.proj-window:hover .proj-address { color: var(--ink); }
.proj-window:hover .proj-address svg { transform: translate(2px, -2px); }

.proj-view { overflow: hidden; border-radius: 0 0 calc(var(--r-md) - 1px) calc(var(--r-md) - 1px); }

.proj-shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top center;
  transition: scale 1.1s var(--ease);
}

.proj-window:hover .proj-shot { scale: 1.03; }

/* As a window scrolls in, its page settles into the frame, from slightly
   close to exactly fitted. Transform only, and only where the browser can
   tie it to scroll. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .proj-shot {
      animation: shot-settle linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 45%;
    }
  }
}

@keyframes shot-settle {
  from { transform: scale(1.12) translateY(-3%); }
  to   { transform: none; }
}

.proj-info { min-width: 0; }

.proj-head { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 1rem; margin-bottom: 0.9rem; }
.proj-index { font-size: 0.75rem; color: var(--subtle); font-variant-numeric: tabular-nums; }
.proj-kind { font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--subtle); }

/* Status, stated and still. A dot that pulses claims a liveness nothing on
   the page is measuring. */
.status-beacon {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin-left: auto;
  padding: 0.2rem 0.65rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface-1);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
}

.beacon-dot { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: var(--subtle); }
.status-beacon.live .beacon-dot { background: var(--verdigris); }
.status-beacon.build .beacon-dot { background: var(--rubric); }

.proj-title { font-size: clamp(1.8rem, 3.2vw, 2.6rem); font-weight: 500; letter-spacing: -0.035em; margin-bottom: 0.9rem; }

.proj-desc { font-size: 0.97rem; line-height: 1.65; color: var(--muted); max-width: 32rem; margin-bottom: 1.6rem; }

/* The record. Each row slides in behind the one before it. */
.proj-meta { margin: 0 0 1.5rem; border-top: 1px solid var(--line); }

.proj-meta > div {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: 0 1rem;
  padding: 0.65rem 0.5rem;
  border-bottom: 1px solid var(--line);
  transition: background var(--t-fast) var(--ease);
}

.proj-meta > div:hover { background: var(--surface-1); }

.proj-meta dt { margin: 0; font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--subtle); }
.proj-meta dd { margin: 0; font-size: 0.875rem; color: var(--ink); }

.js .proj-meta > div {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 620ms var(--ease), transform 620ms var(--ease), background var(--t-fast) var(--ease);
  transition-delay: calc(var(--r, 0) * 70ms + 180ms), calc(var(--r, 0) * 70ms + 180ms), 0s;
}

.js .proj.in .proj-meta > div { opacity: 1; transform: none; }

.proj-more { font-size: 0.875rem; font-weight: 500; margin: 0; }

@media (max-width: 58rem) {
  .proj { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; }
  .proj-next-art { display: none; }
  .proj:nth-child(even) .proj-window { order: 0; }
}

@media (max-width: 30rem) {
  .status-beacon { margin-left: 0; }
  .proj-meta > div { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; }
}

@media (prefers-reduced-motion: reduce) {
  .proj-window:hover .proj-shot { scale: none; }
}

/* ── Contact block, profile.html ────────────────────────────────────────── */


/* ── Contact ─────────────────────────────────────────────────────────────── */
/* The page's last section and its only ask. It used to be two: this form,
   then a closing call to action whose one button linked back up to it. Now
   the ask, the person who answers and the form are one room, lit from below
   by the horizon that section used to carry.

   The form stays ruled rather than boxed: a line under each field instead of
   a container around it, and the line draws in cobalt from the left as the
   field takes focus. Everything the script adds is enhancement. Without it
   the form is a plain POST that lands on thanks.html; with it, a visitor gets
   a check as each field is left, a count on the message, a rule above the
   send button that fills as the three required answers are given, and, on
   success, a receipt of what was sent in place of the form. */

.contact {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: clamp(5.5rem, 9vw, 8rem) clamp(7rem, 12vw, 10rem);
}

.contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2.5rem, 7vw, 6rem);
  align-items: start;
}

.contact-title {
  font-size: clamp(2.6rem, 5vw, 4.25rem);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.045em;
  margin: 0 0 1.25rem;
  text-wrap: balance;
}

.contact-aside .lede { margin-bottom: 2.25rem; }

/* The person who answers, and the address that reaches them. */
.contact-person { display: flex; align-items: center; gap: 0.9rem; padding-top: 1.4rem; border-top: 1px solid var(--line); }

.contact-avatar {
  width: 3rem; height: 3rem;
  border-radius: 50%;
  object-fit: cover; object-position: 50% 15%;
  border: 1px solid var(--line-strong);
  flex-shrink: 0;
}

.contact-person-meta strong { display: block; font-size: 0.9375rem; font-weight: 500; color: var(--ink); }
.contact-person-meta span { display: block; margin-top: 0.15rem; font-size: 0.8125rem; line-height: 1.45; color: var(--subtle); }

.contact-mail { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.75rem; margin: 0.9rem 0 1.5rem; }

.contact-address {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  font-size: 1.05rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: 0.3em;
  overflow-wrap: anywhere;
  transition: text-decoration-color var(--t-fast) var(--ease);
}

.contact-address:hover { text-decoration-color: currentColor; }

.copy-mail {
  min-height: 2.75rem;
  padding: 0 0.95rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: transparent;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}

.copy-mail:hover { color: var(--ink); border-color: var(--line-strong); }
.copy-mail:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.copy-mail.copied { color: var(--verdigris); border-color: var(--success-line); }

/* The facts that decide whether someone writes at all. */
.contact-facts { margin: 0; border-top: 1px solid var(--line); }

.contact-facts > div {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: 0 1rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid var(--line);
}

.contact-facts dt { margin: 0; padding-top: 0.15rem; font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--subtle); }
.contact-facts dd { margin: 0; font-size: 0.875rem; color: var(--ink); }

.enquiry { margin: 0; }

/* Numbered like the rest of the page, so the form reads as part of the same
   document rather than a widget dropped into it. */
.f-num {
  display: block;
  margin-bottom: 0.55rem;
  font-size: 0.8125rem;
  letter-spacing: 0.01em;
  color: var(--subtle);
  font-variant-numeric: tabular-nums;
}

.tracks { border: 0; padding: 0; margin: 0 0 2rem; min-width: 0; }
.tracks legend { padding: 0; display: contents; }

.chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.chip {
  position: relative;
  padding: 0.5rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: 0.85rem;
  color: var(--muted);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.chip:hover { border-color: var(--line-strong); color: var(--ink); }
.chip:has(input:checked) { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.chip:has(input:focus-visible) { outline: 1px solid var(--accent); outline-offset: 3px; }
.chip input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }

/* The plan is a softer choice than the service, so it is marked in the
   pricing's cobalt rather than filled in ink. */
.plans .chip:has(input:checked) { background: var(--accent-soft); border-color: var(--accent-line); color: var(--ink); }

.field-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 2rem; }
.field { display: block; margin: 0 0 1.9rem; max-width: none; }
/* A field keeps its space even as the last paragraph in its row, where the
   general last-child rule would take it away on a phone. */
.field-grid > .field { margin-bottom: 1.9rem; }

/* Ruled, not boxed. The rule is the field, and on focus a cobalt line draws
   along it from the left. */
.field input,
.field textarea {
  width: 100%;
  padding: 0.55rem 0 0.65rem;
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  border-radius: 0;
  background: linear-gradient(var(--accent), var(--accent)) no-repeat 0 100% / 0 2px;
  font: inherit;
  font-size: 1.05rem;
  color: var(--ink);
  transition: border-color var(--t-mid) var(--ease), background-size 480ms var(--ease);
}

.field textarea { resize: vertical; min-height: 5.5rem; line-height: 1.6; }
.field input::placeholder, .field textarea::placeholder { color: var(--subtle); }
.field input:hover, .field textarea:hover { border-bottom-color: var(--line-hover); }
.field input:focus, .field textarea:focus { outline: none; background-size: 100% 2px; }
.field input[aria-invalid="true"], .field textarea[aria-invalid="true"] { border-bottom-color: var(--rubric); }

.field-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-top: 0.55rem; }
.field-hint { display: block; font-size: 0.78rem; line-height: 1.45; color: var(--subtle); }
.char-count { flex: none; font-size: 0.75rem; color: var(--subtle); font-variant-numeric: tabular-nums; white-space: nowrap; transition: color var(--t-fast) var(--ease); }
.char-count.near { color: var(--rubric); }
.field-error { display: block; margin-top: 0.55rem; font-size: 0.78rem; color: var(--rubric); }

/* Off-screen rather than display:none, because some bots skip fields that are
   not rendered at all. */
.trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-foot {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.5rem;
  margin-top: 2.25rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--line);
}

.submit {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  height: 2.75rem;
  padding: 0 0.35rem 0 1.25rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: var(--ink);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--on-ink);
  white-space: nowrap;
  cursor: pointer;
  transition: background 240ms var(--ease), opacity 240ms var(--ease);
}

.submit:hover { background: var(--ink-hover); }
.submit-label { display: inline-block; }

.submit-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.05rem;
  height: 2.05rem;
  border-radius: 50%;
  background: var(--on-ink);
  color: var(--ink);
  position: relative;
  overflow: hidden;
}

.submit-arrow svg { width: 0.75rem; height: 0.75rem; }

/* The same slide as every other circular arrow on the site: the glyph leaves
   to the right and its copy arrives from the left. site.js adds the copy and
   .arrow-swap; without them, or with reduced motion, the arrow sits still. */
.submit:hover .arrow-swap svg,
.submit:focus-visible .arrow-swap svg { transform: translateX(190%); }
.submit:hover .arrow-swap svg + svg,
.submit:focus-visible .arrow-swap svg + svg { transform: translateX(0); }
.enquiry[aria-busy="true"] .submit { opacity: 0.6; pointer-events: none; }

.form-note { margin: 0; flex: 1 1 15rem; font-size: 0.78rem; line-height: 1.5; color: var(--subtle); }

.form-status { margin: 1.5rem 0 0; padding: 0.85rem 1rem; border-radius: var(--r-sm); font-size: 0.9rem; max-width: none; }
.form-status.err { color: var(--rubric); background: var(--pending-soft); border: 1px solid var(--rubric); }
.form-status.ok { color: var(--verdigris); background: var(--success-soft); border: 1px solid var(--success-line); }

/* The receipt that replaces the form once it has gone: what was sent, where
   the answer will go, and a way to send another. */
.receipt {
  padding: clamp(1.75rem, 4vw, 2.5rem);
  border: 1px solid var(--success-line);
  border-radius: var(--r-md);
  background: var(--surface-1);
  animation: receipt-in 520ms var(--ease) both;
}

.receipt-mark {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: 1.25rem;
  border-radius: 50%;
  background: var(--success-soft);
  color: var(--verdigris);
}

.receipt-mark svg { width: 1.2rem; height: 1.2rem; clip-path: inset(0 100% 0 0); animation: tick-draw 520ms var(--ease) 180ms forwards; }

.receipt-title { margin: 0 0 0.4rem; font-size: clamp(1.25rem, 2.2vw, 1.6rem); font-weight: 500; letter-spacing: -0.02em; color: var(--ink); max-width: none; }
.receipt-title:focus { outline: none; }
.receipt-to { margin: 0 0 1.5rem; font-size: 0.9rem; color: var(--muted); overflow-wrap: anywhere; max-width: none; }

.receipt-list { margin: 0 0 1.5rem; border-top: 1px solid var(--line); }
.receipt-list > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 0 1rem; padding: 0.65rem 0; border-bottom: 1px solid var(--line); }
.receipt-list dt { margin: 0; font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--subtle); }
.receipt-list dd { margin: 0; font-size: 0.875rem; color: var(--ink); overflow-wrap: anywhere; }

.receipt-again {
  min-height: 2.75rem;
  padding: 0 1.1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: transparent;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease);
}

.receipt-again:hover { border-color: var(--ink); }

@keyframes receipt-in { from { opacity: 0; transform: translateY(12px); } }
@keyframes tick-draw { to { clip-path: inset(0); } }

@media (prefers-reduced-motion: reduce) {
  .receipt, .receipt-mark svg { animation: none; clip-path: none; }
  .field input, .field textarea { transition: none; }
}

@media (max-width: 62rem) {
  .contact-grid { grid-template-columns: minmax(0, 1fr); gap: 3rem; }
}

@media (max-width: 40rem) {
  .field-grid { grid-template-columns: minmax(0, 1fr); }
  .field-meta { flex-direction: column; gap: 0.35rem; }
}

/* ── Theme ───────────────────────────────────────────────────────────────── */
/* System, Light, Dark. System is the default and follows the device; the
   other two are remembered (one localStorage entry, disclosed on the privacy
   page). The thumb slides under the chosen option, and the new theme grows
   out of the button that was pressed rather than snapping (site.js). */

.hero { position: relative; }

.theme-switch {
  position: relative;
  display: inline-flex; gap: 2px; padding: 3px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
.theme-switch[hidden] { display: none; }

/* The thumb. Three fixed-width options, so its position is a transform and
   never a width or left animation. */
.theme-switch::before {
  content: ""; position: absolute; top: 3px; bottom: 3px; left: 3px;
  width: 2.5rem; border-radius: var(--r-pill);
  background: var(--ink);
  transition: transform var(--t-mid) var(--ease);
}
.theme-switch[data-active="light"]::before { transform: translateX(calc(2.5rem + 2px)); }
.theme-switch[data-active="dark"]::before { transform: translateX(calc(5rem + 4px)); }

.theme-switch button {
  position: relative; z-index: 1;
  width: 2.5rem; height: 2.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-pill);
  background: transparent; color: var(--subtle);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}
.theme-switch button svg { width: 1.05rem; height: 1.05rem; transition: transform var(--t-slow) var(--ease); }
.theme-switch button:hover { color: var(--ink); }
.theme-switch button[aria-pressed="true"] { color: var(--on-ink); }
.theme-switch button:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }

/* Each icon moves once when chosen: the sun turns, the moon tilts. */
.theme-switch button[data-theme-set="light"][aria-pressed="true"] svg { transform: rotate(90deg); }
.theme-switch button[data-theme-set="dark"][aria-pressed="true"] svg { transform: rotate(-18deg); }

/* Under the footer brand. */
.footer-theme { margin-top: 1.25rem; }

/* site.js animates the new theme outward from the pressed button; the
   default cross-fade is switched off so the two never fight. */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

/* Light-only adjustments that are not a colour swap. The portrait print is
   pale on a pale page, so it takes a hairline edge. */
:root[data-theme="light"] .prof-portrait { box-shadow: inset 0 0 0 1px var(--line); }

@media (prefers-reduced-motion: reduce) {
  .theme-switch::before, .theme-switch button svg { transition: none; }
}

/* ── From the hero into the page ─────────────────────────────────────────── */
/* The light becomes the rule. As the hero leaves, its content recedes - a
   touch smaller, lifted, dimmed - and as the next section arrives, a line of
   the hero's cobalt draws across its top edge from the centre and settles
   into the same faint hairline every other section is built on. The
   atmosphere does not stop; it hands over.

   Scroll-driven, CSS only: no scroll listener, and every frame is transform
   and opacity on the compositor. Browsers without animation-timeline, and
   anyone who asked for less motion, get the finished static page. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero > .wrap {
      animation: hero-recede linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }

    .hero + section { position: relative; }
    .hero + section::before {
      content: "";
      position: absolute; top: 0; left: 50%;
      width: min(100% - 2 * var(--gutter), var(--container));
      height: 1px;
      translate: -50% 0;
      background: linear-gradient(90deg, transparent, var(--accent) 50%, transparent);
      transform-origin: center;
      animation: rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 85%;
      pointer-events: none;
    }
  }
}

@keyframes hero-recede {
  to { transform: scale(0.955) translateY(-3vh); opacity: 0.55; }
}

@keyframes rule-draw {
  0%   { transform: scaleX(0); opacity: 1; }
  65%  { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0.4; }
}

/* ── From the promises into the prices ───────────────────────────────────── */
/* A stacked scroll. The promises and the prices share one wrapper, .stack.
   Once the promises have been read to their last line they pin there, and
   the pricing sheet slides up over them like the next sheet on a pile: it
   opens from a rounded sheet, inset to the content's edges, into the full
   width, while the promises beneath settle back and dim. Past the prices,
   the pair leaves together, because a sticky element cannot outlive its
   wrapper.

   site.js measures the promises and sets --stick, the offset that pins them
   by their bottom edge rather than their top, so a section taller than the
   screen is read in full before anything covers it. It adds .stacked only
   once that number exists: without JavaScript nothing pins. The settle and
   the opening read one timeline, the pricing section's passage through the
   viewport, which timeline-scope shares across the pair, so the two cannot
   drift apart. Browsers without scroll timelines still get the pin and the
   cover; reduced motion gets neither. */

.stack { position: relative; }
.stack.stacked > section:first-child { position: sticky; top: var(--stick, 0px); }
.stack > #pricing { z-index: 1; }

@supports (animation-timeline: view()) and (timeline-scope: --a) {
  @media (prefers-reduced-motion: no-preference) {
    .stack { timeline-scope: --pricing; }
    #pricing { view-timeline: --pricing block; }

    #pricing::before {
      animation: panel-open linear both;
      animation-timeline: --pricing;
      animation-range: cover 0vh cover 85vh;
    }

    .stack > section:first-child > .wrap {
      transform-origin: 50% 100%;
      animation: promises-settle linear both;
      animation-timeline: --pricing;
      animation-range: cover 0vh cover 85vh;
    }
  }
}

@keyframes panel-open {
  from { clip-path: inset(0 var(--gutter) 0 var(--gutter) round 2rem 2rem 0 0); }
  to   { clip-path: inset(0 0 0 0 round 0); }
}

@keyframes promises-settle {
  to { transform: scale(0.94); opacity: 0.35; }
}

/* A link into the terms lands on its clause, clear of the floating bar. */
.legal h2[id] { scroll-margin-top: 6.5rem; }

/* ── Process ─────────────────────────────────────────────────────────────── */

#process { position: relative; isolation: isolate; }

/* The blueprint. The section's ground arrives as a grid of square cells in
   the page's own colour, each outlined in a hairline, and, tied to the
   scroll, the cells turn away like a split-flap board - each row as it
   crosses the lower part of the screen, the centre first and the edges
   after, so the wave is a V opening down the page - each cell's edge
   lighting cobalt as it turns - leaving the section's own ground: a shade
   off the page, the same grid drawn faintly across it, and cobalt light
   along its top edge. Reversible with the scroll. The words always stand
   above the cells, so the transition never hides a line of copy.

   site.js lays the cells (--cols, --cell, and each cell's --h, its
   distance from the centre column). Without it, without scroll timelines, or with reduced motion,
   there are no cells and the finished ground is simply there. */
.process-ground {
  --cell: 7.5rem;
  position: absolute;
  inset: 0;
  z-index: -1;
  /* clip, not hidden: hidden would make this a scroll container, and every
     cell's view() timeline would then measure the cell against this box
     instead of against the screen, and read as already finished. */
  overflow: clip;
  background: var(--panel);
  border-top: 1px solid var(--line-strong);
  pointer-events: none;
}

.process-ground::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: var(--cell) var(--cell);
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent 65%);
  mask-image: linear-gradient(to bottom, #000, transparent 65%);
}

.process-ground::after {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: min(80rem, 110%);
  height: 30rem;
  translate: -50% -55%;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--highlight), transparent);
  opacity: var(--atmos);
}

.process-mosaic {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(var(--cols, 12), var(--cell));
}

.process-mosaic span {
  position: relative;
  height: var(--cell);
  background: var(--paper);
  box-shadow: inset 0 0 0 0.5px var(--line);
  backface-visibility: hidden;
}

.process-mosaic span::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--accent);
  opacity: 0;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Each cell keeps its own timeline, so a row turns as it crosses the
       lower part of the screen, where it can be seen, and the centre columns
       lead the outer ones by --h, the cell's distance from the centre. */
    .process-mosaic span,
    .process-mosaic span::after {
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: cover calc(var(--h, 0) * 2.6vh) cover calc(var(--h, 0) * 2.6vh + 24vh);
    }

    .process-mosaic span { animation-name: cell-turn; }
    .process-mosaic span::after { animation-name: cell-edge; }
  }
}

@keyframes cell-turn {
  0%   { transform: perspective(40rem) rotateX(0deg); opacity: 1; }
  55%  { opacity: 1; }
  100% { transform: perspective(40rem) rotateX(88deg) scale(0.9); opacity: 0; }
}

@keyframes cell-edge {
  0%   { opacity: 0; }
  35%  { opacity: 0.9; }
  100% { opacity: 0; }
}

/* ── Bento ───────────────────────────────────────────────────────────────── */
/* Process and the promises are both bento grids: tiles of different sizes on
   one twelve-column grid, each tile holding a sentence and a picture of that
   sentence. The pictures are not decoration. The scope sheet lists the
   Essential plan's published inclusions and the FAQ's own "what I do not do";
   the twelve cells under Running are the twelve-month minimum term; the
   ownership split is clause 7's two lists. A tile never shows a thing the
   terms or the page do not already say.

   Motion, in three layers, all switched off for reduced motion:
     arrival  each tile drifts in from its own side of the grid - left, right,
              up, or along a diagonal toward its corner - so the grid
              assembles rather than fades (site.js adds .in);
     detail   inside a tile, its picture builds itself once: the sheet fills,
              the checks tick, the months light, the phone lays out;
     scroll   a tile's picture keeps a slow drift tied to the scroll, a little
              across and a little up, so the grid never feels pasted down.
   And with a mouse, a light follows the pointer over a tile and along its
   edge. */

.bento {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(0.75rem, 1.2vw, 1rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tile {
  --sx: 50%;
  --sy: 0%;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
  min-width: 0;
  padding: clamp(1.35rem, 2.3vw, 1.9rem);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  transition: border-color var(--t-mid) var(--ease),
              box-shadow var(--t-mid) var(--ease),
              transform var(--t-mid) var(--ease);
}

.process .tile { background: var(--panel-raise); }

.tile:hover {
  border-color: var(--line-strong);
  box-shadow: 0 30px 60px -34px var(--shadow-strong);
  transform: translateY(-3px);
}

/* The pointer's light: a wash inside the tile, and a brighter arc on the
   edge nearest the pointer. site.js moves --sx and --sy. */
.tile::before,
.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-mid) var(--ease);
}

.tile::before {
  z-index: -1;
  background: radial-gradient(30rem circle at var(--sx) var(--sy), var(--accent-soft), transparent 62%);
}

.tile::after {
  padding: 1px;
  background: radial-gradient(16rem circle at var(--sx) var(--sy), var(--accent), transparent 70%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}

.tile:hover::before { opacity: 1; }
.tile:hover::after { opacity: 0.85; }

.tile-copy { display: grid; gap: 0.6rem; }

.tile-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.tile-num { font-size: 0.75rem; letter-spacing: 0.08em; color: var(--subtle); font-variant-numeric: tabular-nums; }

.tile-tag {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
}

.tile-title {
  font-size: clamp(1.25rem, 1.9vw, 1.6rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.15;
}

.tile-text { margin: 0; font-size: 0.9rem; line-height: 1.65; color: var(--muted); max-width: 34rem; }

.tile-foot { margin-top: auto; display: grid; gap: 0.9rem; }

.tile-roles { display: grid; gap: 0.35rem; margin: 0; padding-top: 0.85rem; border-top: 1px solid var(--line); font-size: 0.8rem; }
.tile-roles > div { display: grid; grid-template-columns: 4.5rem minmax(0, 1fr); gap: 0.75rem; }
.tile-roles dt { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--subtle); }
.tile-roles dd { margin: 0; color: var(--muted); }

.tile-out { position: relative; margin: 0; padding-top: 0.85rem; border-top: 1px solid var(--line); font-size: 0.9rem; font-weight: 500; color: var(--ink); max-width: none; }
.tile-kicker { display: block; margin-bottom: 0.2rem; font-size: 0.75rem; font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); }

/* The outcome is underlined in rubric once the tile has arrived. */
.tile-out::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: 2.4rem;
  height: 2px;
  background: var(--rubric);
}

.tile-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  align-self: flex-start;
  min-height: 2.75rem;
  margin-top: auto;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: 0.3em;
  transition: text-decoration-color var(--t-fast) var(--ease);
}

.tile-link svg { width: 0.7rem; height: 0.7rem; transition: transform var(--t-mid) var(--ease); }
.tile-link:hover { color: var(--ink); text-decoration-color: currentColor; }
.tile-link:hover svg { transform: translate(2px, -2px); }

.clause-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.7rem;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
}

.clause-badge.practice { border-color: var(--line-strong); background: transparent; color: var(--muted); }

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

/* ── Process, as a bento ───────────────────────────────────────────────── */
/* A zig-zag: the wide tile swaps sides on the second row, so the eye runs
   down the grid on a diagonal - scope, prototype, launch, running - which is
   the order of the work. Above it, a rail of the four steps lights up in
   turn as the grid passes through the screen. */

.bento-process > .tile-scope  { grid-column: 1 / span 7; }
.bento-process > .tile-proto  { grid-column: 8 / span 5; }
.bento-process > .tile-launch { grid-column: 1 / span 5; }
.bento-process > .tile-run    { grid-column: 6 / span 7; }

.process-rail {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
  margin: 0 0 clamp(1.25rem, 2.5vw, 1.75rem);
  padding: 0 0 0.9rem;
  list-style: none;
  border-bottom: 1px solid var(--line);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--subtle);
}

.process-rail li { display: flex; align-items: center; gap: 0.6rem; }
.process-rail svg { width: 0.8rem; height: 0.8rem; fill: var(--accent); }
.rail-num { font-size: 0.75rem; font-weight: 400; font-variant-numeric: tabular-nums; }

.process-rail::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg, var(--accent) 85%, transparent);
  transform-origin: left center;
}

/* The scope sheet: a page, set a shade lighter than its tile, with the two
   lists a quote is made of. */
.sheet {
  display: grid;
  gap: 1rem;
  padding: clamp(1rem, 1.8vw, 1.35rem);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: 0 24px 50px -30px var(--shadow-strong);
}

.sheet-top,
.sheet-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  font-size: 0.75rem;
  color: var(--subtle);
}

.sheet-top { padding-bottom: 0.75rem; border-bottom: 1px solid var(--line); }
.sheet-plan { font-variant-numeric: tabular-nums; }
.sheet-label { font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink); }
.sheet-foot { padding-top: 0.75rem; border-top: 1px solid var(--line); }

.sheet-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; }
.sheet-col { min-width: 0; }
.sheet-head { margin: 0 0 0.6rem; font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); }

.sheet-list { display: grid; gap: 0.5rem; margin: 0; padding: 0; list-style: none; font-size: 0.8125rem; color: var(--ink); }
.sheet-list li { display: flex; align-items: flex-start; gap: 0.6rem; line-height: 1.4; }

/* A tick and a cross, drawn in CSS so neither needs a file or a colour of
   its own. */
.sheet-list li::before {
  content: "";
  flex: none;
  width: 0.95rem;
  height: 0.95rem;
  margin-top: 0.05rem;
  border-radius: 50%;
}

.sheet-in li::before {
  background:
    linear-gradient(135deg, transparent 46%, var(--verdigris) 46% 58%, transparent 58%) 0.36rem 0.22rem / 0.3rem 0.42rem no-repeat,
    var(--success-soft);
  box-shadow: inset 0 0 0 1px var(--success-line);
}

.sheet-out li { color: var(--muted); }
.sheet-out li::before {
  background:
    linear-gradient(45deg, transparent 45%, var(--rubric) 45% 55%, transparent 55%) center / 0.45rem 0.45rem no-repeat,
    linear-gradient(-45deg, transparent 45%, var(--rubric) 45% 55%, transparent 55%) center / 0.45rem 0.45rem no-repeat,
    var(--pending-soft);
}

.sheet-stamp {
  padding: 0.3rem 0.75rem;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--ink);
}

/* The prototype: a phone, drawn, laying out a page. It bleeds off the
   tile's lower edge the way a device photographed on a table does. */
.tile-proto .tile-foot { position: relative; z-index: 1; }

.phone {
  position: relative;
  align-self: center;
  display: grid;
  align-content: start;
  gap: 0.45rem;
  width: min(11rem, 58%);
  aspect-ratio: 9 / 14;
  margin-block: 0.25rem -0.5rem;
  padding: 2.1rem 0.85rem 0.85rem;
  border: 1px solid var(--line-strong);
  border-radius: 2rem;
  background: var(--surface-2);
  box-shadow: 0 40px 70px -40px var(--shadow-strong), inset 0 0 0 5px var(--surface-1);
  -webkit-mask-image: linear-gradient(to bottom, #000 72%, transparent);
  mask-image: linear-gradient(to bottom, #000 72%, transparent);
}

.phone-island {
  position: absolute;
  top: 0.8rem;
  left: 50%;
  width: 3.4rem;
  height: 0.8rem;
  translate: -50% 0;
  border-radius: var(--r-pill);
  background: var(--paper);
}

.phone-bar { height: 0.55rem; width: 55%; margin: 0 auto 0.35rem; border-radius: var(--r-pill); background: var(--surface-3); }
.phone-block { display: block; border-radius: 0.4rem; background: var(--surface-3); transform-origin: left center; }
.phone-hero { height: 3.4rem; background: linear-gradient(135deg, var(--accent-soft), var(--surface-3)); }
.phone-line { height: 0.5rem; width: 88%; }
.phone-line.short { width: 60%; }
.phone-cta { height: 1.25rem; width: 46%; border-radius: var(--r-pill); background: var(--ink); }
.phone-card { height: 2.6rem; }

/* A tap on the call to action, twice: the visitor trying it on their phone. */
.phone-tap {
  position: absolute;
  top: 8.3rem;
  left: 2.6rem;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--accent);
  border-radius: 50%;
  opacity: 0;
}

/* Launch: the four things the copy says are tested, then the switch. */
.checks { display: grid; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }

.checks li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 0.15rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.875rem;
  color: var(--ink);
}

.check-state {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--verdigris);
}

.check-state::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: currentColor;
}

.check-final { background: linear-gradient(90deg, var(--accent-soft), transparent); }
.check-final .check-name { font-weight: 500; }
.check-final .check-state { color: var(--ink); }
.check-final .check-state::before { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }

/* Running: the twelve months of the minimum term, then the turn into month
   to month. */
.term-months { display: grid; gap: 0.75rem; }

.month-strip {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0.3rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.month-strip li {
  display: grid;
  place-items: end center;
  height: 3.25rem;
  padding-bottom: 0.35rem;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-xs);
  background: var(--accent-soft);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.month-note { display: flex; align-items: center; gap: 0.5rem; margin: 0; font-size: 0.8125rem; color: var(--muted); max-width: none; }
.month-note svg { flex: none; width: 0.85rem; height: 0.85rem; fill: var(--accent); }

.care-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; padding: 0; list-style: none; }
.care-list li { padding: 0.25rem 0.7rem; border: 1px solid var(--line); border-radius: var(--r-pill); font-size: 0.75rem; color: var(--muted); }

@media (max-width: 62rem) {
  .bento-process > .tile-scope,
  .bento-process > .tile-run { grid-column: 1 / -1; }
  .bento-process > .tile-proto { grid-column: 1 / span 6; }
  .bento-process > .tile-launch { grid-column: 7 / span 6; }
}

@media (max-width: 44rem) {
  .process-rail { display: none; }
  .sheet-cols { grid-template-columns: minmax(0, 1fr); }
  .month-strip li { height: 2.5rem; font-size: 0.75rem; }
}

@media (max-width: 26rem) {
  .month-strip { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* ── The promises, as a bento ────────────────────────────────────────────── */
/* Where testimonials would go, the promises a client can check. The largest
   tile carries the contract's own words with the operative phrase marked, as
   someone reading the terms closely would mark it. And one tile is left
   empty on purpose, saying why: clause 7 forbids naming a client without
   their written agreement, so the first real quote will go there and nothing
   stands in for it until then. */

.bento-promises > .tile-quote { grid-column: 1 / span 7; grid-row: span 2; }
.bento-promises > .tile-own   { grid-column: 8 / span 5; }
.bento-promises > .tile-leave { grid-column: 8 / span 5; }
.bento-promises > .tile-one   { grid-column: 1 / span 4; }
.bento-promises > .tile-slot  { grid-column: 5 / span 4; }
.bento-promises > .tile-sign  { grid-column: 9 / span 4; }

.tile-quote .tile-title { font-size: clamp(1.6rem, 3vw, 2.5rem); line-height: 1.08; letter-spacing: -0.035em; max-width: 16ch; }

.excerpt {
  position: relative;
  margin: 0;
  padding: clamp(1.25rem, 2.2vw, 1.75rem);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.excerpt::before {
  content: "";
  position: absolute;
  top: clamp(1.25rem, 2.2vw, 1.75rem);
  bottom: clamp(1.25rem, 2.2vw, 1.75rem);
  left: 0;
  width: 2px;
  background: var(--rubric);
}

/* The quote, as a ledger: what is written into it, and the one line that
   is not - anything outside it, quoted before it is built. */
.ledger { display: grid; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }

.ledger li {
  display: flex;
  align-items: baseline;
  padding: 0.7rem 0.15rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.875rem;
  color: var(--ink);
}

/* A dotted leader between the item and its line, as on a printed quote. */
.ledger li::after {
  content: "";
  order: 1;
  flex: 1;
  min-width: 1.5rem;
  margin: 0 0.75rem 0.3em;
  border-bottom: 1px dotted var(--line-hover);
}

.ledger-value { order: 2; font-size: 0.8125rem; color: var(--muted); text-align: right; }
.ledger-extra { background: linear-gradient(90deg, var(--accent-soft), transparent); }
.ledger-extra .ledger-value { color: var(--ink); font-weight: 500; }

.tile-quote .tile-link { margin-top: 0; }
.tile-quote .ledger { margin-top: auto; }

.excerpt-ref { display: block; margin-bottom: 0.75rem; font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); }
.excerpt p { margin: 0; font-size: clamp(1rem, 1.35vw, 1.15rem); line-height: 1.65; color: var(--ink); max-width: none; }

/* The marked phrase. A highlighter stroke laid down left to right. */
.swipe {
  color: var(--ink);
  background: linear-gradient(var(--highlight), var(--highlight)) no-repeat 0 88% / 100% 42%;
  padding-inline: 0.1em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Ownership, as clause 7 divides it. */
.split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.split-col { min-width: 0; }
.split-licensed { padding-left: 1rem; border-left: 1px solid var(--line); }
.split-head { margin: 0 0 0.6rem; font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--subtle); }
.split-list { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0; padding: 0; list-style: none; }

.split-list li {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.65rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-size: 0.75rem;
  color: var(--ink);
}

.split-list li::before { content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--verdigris); }
.split-licensed li { color: var(--muted); }
.split-licensed li::before { background: var(--accent); }

/* Leaving: the minimum term as a filled bar, then month to month as an open
   one, and the two ways out beneath. */
.term-bar { display: grid; grid-template-columns: 3fr 2fr; gap: 0.3rem; }

.term-bar > span {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 0.75rem;
  border-radius: var(--r-xs);
  font-size: 0.75rem;
  font-weight: 500;
}

.term-min { color: var(--ink); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-line); }
.term-flex { color: var(--muted); box-shadow: inset 0 0 0 1px var(--line-strong); background: repeating-linear-gradient(135deg, transparent 0 6px, var(--line) 6px 7px); }

.exits { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
.exits li { display: grid; gap: 0.15rem; padding: 0.7rem 0.85rem; border: 1px solid var(--line); border-radius: var(--r-sm); }
.exits strong { font-size: 0.8125rem; font-weight: 500; color: var(--ink); }
.exits span { font-size: 0.75rem; color: var(--subtle); }

/* One project: the numeral, outlined, is the picture. It stands in the
   flow above the claim, so it can never sit behind a line being read. */
.one-figure {
  margin-top: auto;
  font-size: clamp(5.5rem, 8vw, 7.5rem);
  font-weight: 500;
  line-height: 0.8;
  letter-spacing: -0.06em;
  color: transparent;
  -webkit-text-stroke: 1px var(--line-hover);
  user-select: none;
}

.tile-one .tile-text { max-width: 26rem; }

/* The empty chair. Dashed, because it is waiting to be filled. */
.tile-slot { border-style: dashed; border-color: var(--line-strong); background: transparent; }
.slot-mark { font-size: 3.5rem; line-height: 0.6; color: var(--rubric); }
.slot-text { margin: 0; font-size: clamp(1rem, 1.4vw, 1.15rem); line-height: 1.55; color: var(--muted); max-width: none; }
.slot-why { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin: auto 0 0; font-size: 0.8125rem; color: var(--subtle); max-width: none; }

/* The signature. */
.tile-sign img {
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  object-fit: cover;
  object-position: 50% 15%;
}

.sign-name { margin: 0; max-width: none; }
.sign-name strong { display: block; font-size: 0.95rem; font-weight: 500; color: var(--ink); }
.sign-name span { display: block; font-size: 0.75rem; color: var(--subtle); }
.sign-note { margin: 0; font-size: 0.875rem; line-height: 1.6; color: var(--muted); max-width: none; }

@media (max-width: 62rem) {
  .bento-promises > .tile-quote,
  .bento-promises > .tile-sign { grid-column: 1 / -1; grid-row: auto; }
  .bento-promises > .tile-own,
  .bento-promises > .tile-one { grid-column: 1 / span 6; }
  .bento-promises > .tile-leave,
  .bento-promises > .tile-slot { grid-column: 7 / span 6; }
}

@media (max-width: 30rem) {
  .split, .exits { grid-template-columns: minmax(0, 1fr); }
  .split-licensed { padding-left: 0; padding-top: 1rem; border-left: 0; border-top: 1px solid var(--line); }
}

/* One column on a phone, whatever the grid above asked for. Last, so it
   wins over both grids. */
@media (max-width: 44rem) {
  .bento > .tile { grid-column: 1 / -1; grid-row: auto; }
}

.process .head-center,
.promises .head-center { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* ── Bento motion ────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: no-preference) {
  /* Arrival. Sideways travel is held inside the gutter, so a tile in flight
     never reaches past the edge of the screen; on a narrow screen tiles only
     rise, because there is no gutter to travel in. */
  .js .tile[data-drift] {
    --t: calc(var(--gutter) * 0.8);
    opacity: 0;
    translate: var(--dx, 0) var(--dy, 0);
    scale: 0.965;
    transition: opacity 0.9s var(--ease), translate 1.15s var(--ease), scale 1.15s var(--ease),
                border-color var(--t-mid) var(--ease), box-shadow var(--t-mid) var(--ease),
                transform var(--t-mid) var(--ease);
    transition-delay: calc(var(--i, 0) * 90ms), calc(var(--i, 0) * 90ms), calc(var(--i, 0) * 90ms), 0s, 0s, 0s;
    animation: drift-failsafe 1ms linear 4s forwards;
  }

  .js .tile[data-drift="left"]         { --dx: calc(-1 * var(--t)); }
  .js .tile[data-drift="right"]        { --dx: var(--t); }
  .js .tile[data-drift="bottom"]       { --dy: 3rem; }
  .js .tile[data-drift="top-right"]    { --dx: var(--t); --dy: -2.5rem; }
  .js .tile[data-drift="bottom-left"]  { --dx: calc(-1 * var(--t)); --dy: 2.5rem; }
  .js .tile[data-drift="bottom-right"] { --dx: var(--t); --dy: 2.5rem; }

  .js .tile[data-drift].in { opacity: 1; translate: none; scale: none; animation: none; }

  /* site.js is running and will reveal each tile as it arrives, so the timed
     failsafe is withdrawn: without this, every tile further down than four
     seconds of reading would simply appear, never having moved. */
  .js.reveal-live .tile[data-drift]:not(.in) { animation: none; }

  /* Detail. Each picture builds once its tile has arrived; --k is each
     part's place in the order, set by site.js. */
  .reveal-live .tile[data-drift] .sheet-list li,
  .reveal-live .tile[data-drift] .ledger li,
  .reveal-live .tile[data-drift] .checks li {
    opacity: 0;
    translate: -0.6rem 0;
    transition: opacity 0.5s var(--ease), translate 0.6s var(--ease);
    transition-delay: calc(var(--k, 0) * 85ms + 380ms);
  }

  .reveal-live .tile[data-drift] .check-state { opacity: 0; scale: 0.6; transition: opacity 0.4s var(--ease), scale 0.5s var(--ease); transition-delay: calc(var(--k, 0) * 140ms + 700ms); }

  .reveal-live .tile[data-drift] .sheet-stamp { opacity: 0; }
  .reveal-live .tile.in .sheet-stamp { animation: stamp 560ms var(--ease) 1.3s both; }

  .reveal-live .tile[data-drift] .month-strip li {
    background: transparent;
    transition: background 0.45s var(--ease), border-color 0.45s var(--ease);
    transition-delay: calc(var(--k, 0) * 75ms + 450ms);
    border-color: var(--line);
  }

  .reveal-live .tile[data-drift] .phone-block { scale: 0 1; transition: scale 0.7s var(--ease); transition-delay: calc(var(--k, 0) * 110ms + 400ms); }
  .reveal-live .tile.in .phone-tap { animation: tap 1.6s var(--ease) 1.4s 2; }

  .reveal-live .tile[data-drift] .swipe { background-size: 0% 42%; transition: background-size 1.3s var(--ease) 0.7s; }

  .reveal-live .tile[data-drift] .split-yours li { opacity: 0; translate: -0.75rem 0; }
  .reveal-live .tile[data-drift] .split-licensed li { opacity: 0; translate: 0.75rem 0; }
  .reveal-live .tile[data-drift] .split-list li { transition: opacity 0.5s var(--ease), translate 0.6s var(--ease); transition-delay: calc(var(--k, 0) * 70ms + 380ms); }

  .reveal-live .tile[data-drift] .term-min::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: var(--accent-soft);
    transform-origin: left center;
    scale: 0 1;
    transition: scale 1.1s var(--ease) 0.5s;
  }

  .reveal-live .tile[data-drift] .one-figure { opacity: 0; translate: 1.5rem -1.5rem; transition: opacity 1s var(--ease) 0.3s, translate 1.3s var(--ease) 0.3s; }

  .reveal-live .tile.in .sheet-list li,
  .reveal-live .tile.in .ledger li,
  .reveal-live .tile.in .checks li,
  .reveal-live .tile.in .split-list li { opacity: 1; translate: none; }
  .reveal-live .tile.in .check-state { opacity: 1; scale: none; }
  .reveal-live .tile.in .month-strip li { background: var(--accent-soft); border-color: var(--accent-line); }
  .reveal-live .tile.in .phone-block { scale: none; }
  .reveal-live .tile.in .swipe { background-size: 100% 42%; }
  .reveal-live .tile.in .term-min::before { scale: none; }
  .reveal-live .tile.in .one-figure { opacity: 1; translate: none; }
}

@keyframes drift-failsafe { to { opacity: 1; translate: none; scale: none; } }

@keyframes stamp {
  0%   { opacity: 0; transform: scale(1.35) rotate(-7deg); }
  55%  { opacity: 1; transform: scale(0.96) rotate(0deg); }
  100% { opacity: 1; transform: none; }
}

@keyframes tap {
  0%   { opacity: 0; transform: scale(0.4); }
  25%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.9); }
}

@media (max-width: 62rem) {
  .js .tile[data-drift] { --t: 0px; }
  .js .tile[data-drift="top-right"] { --dy: 2.5rem; }
}

/* Scroll. Each picture keeps a slow diagonal drift across the screen, tied
   to the scroll and reversing with it. A few pixels either way: enough to
   make the grid feel set in depth, never enough to move a line being read. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .sheet, .phone, .checks, .term-months, .excerpt, .split, .one-figure {
      animation: drift-diagonal linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    .phone, .one-figure { animation-name: drift-diagonal-far; }
    .checks, .split { animation-name: drift-diagonal-back; }

    /* The rail lights each step as the grid passes the middle of the screen,
       and its line fills across the same passage. */
    .process .wrap { timeline-scope: --bento; }
    .bento-process { view-timeline: --bento block; }

    .process-rail::after {
      animation: rail-fill linear both;
      animation-timeline: --bento;
      animation-range: cover 5% cover 60%;
    }

    .process-rail li {
      animation: rail-lit linear both;
      animation-timeline: --bento;
    }

    .process-rail li:nth-child(1) { animation-range: cover 5% cover 8%; }
    .process-rail li:nth-child(2) { animation-range: cover 19% cover 22%; }
    .process-rail li:nth-child(3) { animation-range: cover 33% cover 36%; }
    .process-rail li:nth-child(4) { animation-range: cover 47% cover 50%; }
  }
}

@keyframes drift-diagonal      { from { transform: translate(0.6rem, 1.1rem); }  to { transform: translate(-0.6rem, -1.1rem); } }
@keyframes drift-diagonal-back { from { transform: translate(-0.5rem, 0.9rem); } to { transform: translate(0.5rem, -0.9rem); } }
@keyframes drift-diagonal-far  { from { transform: translate(-1rem, 2.4rem) rotate(-3deg); } to { transform: translate(0.8rem, -1.2rem) rotate(1.5deg); } }
@keyframes rail-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes rail-lit  { to { color: var(--ink); } }

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

/* ── Pricing, arriving ───────────────────────────────────────────────────── */
/* The tiers arrive the way the layout reads. The recommended plan rises up
   the middle first and settles, then the two outer plans slide in from their
   own sides to flank it, so the eye lands on it before the rest. The travel is a
   fraction of a card's own width, and it is tied to scroll rather than to a
   clock, so it pauses and reverses with the reader, and it plays after the
   pricing sheet has opened rather than on top of it.

   Stacked on a phone, sideways travel would mean sideways scroll, so there
   every tier simply rises as it reaches the reader. The panel clips
   sideways overflow either way, so a card mid-flight can never widen the
   page. These rules replace the timed [data-reveal] fade for the tiers only
   where the browser can tie motion to scroll; elsewhere that fade remains. */
#pricing { overflow-x: clip; }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .js .tier-grid > .tier[data-reveal] {
      opacity: 1;
      transform: none;
      transition: border-color var(--t-mid) var(--ease), box-shadow var(--t-mid) var(--ease), opacity 0.5s var(--ease), scale 0.5s var(--ease);
      animation: tier-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }

    @media (min-width: 62.0625rem) {
      .js .tier-grid > .tier[data-reveal]:nth-child(1) { animation-name: tier-from-left;  animation-range: entry 12% cover 38%; }
      .js .tier-grid > .tier[data-reveal]:nth-child(3) { animation-name: tier-from-right; animation-range: entry 12% cover 38%; }
      .js .tier-grid > .tier[data-reveal]:nth-child(2) { animation-name: tier-rise;       animation-range: entry 0% cover 26%; }
    }
  }
}

@keyframes tier-from-left  { from { opacity: 0; translate: -18% 0; rotate: -1.5deg; } }
@keyframes tier-from-right { from { opacity: 0; translate: 18% 0;  rotate: 1.5deg; } }
@keyframes tier-rise       { from { opacity: 0; translate: 0 14%;  scale: 0.96; } }

/* ── Pricing, to the form ────────────────────────────────────────────────── */
/* Each tier ends in the one thing a visitor who has chosen can do next: go to
   the form with that plan already picked. site.js ticks the plan chip on the
   way, the same way a service's call to action ticks the service. */
.tier-includes { margin-bottom: 1.75rem; }

.tier-cta {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 2.75rem;
  margin-top: auto;
  padding: 0 1.1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.tier-cta svg { width: 0.7rem; height: 0.7rem; transition: transform var(--t-mid) var(--ease); }
.tier-cta:hover { border-color: var(--ink); background: var(--surface-1); }
.tier-cta:hover svg { transform: translate(2px, -2px); }

.tier.featured .tier-cta { border-color: rgba(255, 255, 255, 0.55); color: #fff; }
.tier.featured .tier-cta:hover { border-color: #fff; background: rgba(255, 255, 255, 0.12); }

/* ── Questions, searchable ───────────────────────────────────────────────── */
/* Nine answers are few enough to scan, and a visitor with one worry still
   wants to go straight to it. A search box and three topic filters narrow
   the list as they type; the words that matched are marked; the first match
   opens by itself; and a question the page does not answer turns into a
   button that carries it, typed, into the enquiry form. Every answer can
   also be copied as a link, for the partner who has to agree before anyone
   signs. None of it is shown without JavaScript, where the full list stands
   as it always has. */

.faq-tools { display: grid; gap: 0.9rem; margin-bottom: 2.25rem; }
.faq-tools[hidden] { display: none; }

.faq-search {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0 1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--surface-1);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-mid) var(--ease);
}

.faq-search:focus-within { border-color: var(--accent-line); box-shadow: 0 0 0 4px var(--accent-soft); }
.faq-search svg { flex: none; width: 1rem; height: 1rem; fill: var(--subtle); }

.faq-search input {
  flex: 1;
  min-width: 0;
  min-height: 2.9rem;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 0.9375rem;
  color: var(--ink);
}

.faq-search input:focus { outline: none; }
.faq-search input::placeholder { color: var(--subtle); }
.faq-search input::-webkit-search-cancel-button { filter: grayscale(1); }
.faq-count { flex: none; font-size: 0.75rem; font-variant-numeric: tabular-nums; color: var(--subtle); }

.faq-filters { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.faq-filters button {
  min-height: 2.5rem;
  padding: 0 1rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: transparent;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.faq-filters button:hover { color: var(--ink); border-color: var(--line-strong); }
.faq-filters button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.faq-filters button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.faq[hidden],
.faq-group[hidden] { display: none; }

/* The words that matched, marked the way the terms excerpt marks its
   promise. */
.faq-q { min-width: 0; }

.faq-hit { color: var(--ink); background: var(--highlight); border-radius: 2px; padding-inline: 0.05em; }

.faq-empty {
  display: grid;
  justify-items: start;
  gap: 1rem;
  margin-bottom: 2rem;
  padding: 1.5rem;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
}

.faq-empty[hidden] { display: none; }
.faq-empty p { margin: 0; font-size: 0.9375rem; color: var(--muted); }

/* An open answer is marked down its left edge, in the cobalt. */
.faq { position: relative; }

.faq::before {
  content: "";
  position: absolute;
  top: 1.1rem;
  bottom: 1.1rem;
  left: -1rem;
  width: 2px;
  border-radius: var(--r-pill);
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform var(--t-mid) var(--ease);
}

.faq[open]::before { transform: none; }

.faq-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 2.75rem;
  margin-top: 0.6rem;
  padding: 0 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: transparent;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}

.faq-link:hover { color: var(--ink); border-color: var(--line-strong); }
.faq-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.faq-link.copied { color: var(--verdigris); border-color: var(--success-line); }

@media (max-width: 34rem) {
  .faq::before { left: -0.6rem; }
}

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

/* ── Who you work with: the career, to scale ─────────────────────────────── */
/* Each past role carries a bar as long as the time Eddy states for it, on a
   shared scale of five years, so four years reads as four times one at a
   glance. Lengths only: the profile gives durations, not start dates, and a
   bar placed on a calendar would be inventing years. site.js sets --b from
   data-years; the bars draw in from the left, in order, once the list
   arrives. The studio itself, dated only "since 2026", carries no bar. */

.career-axis { display: none; }

.js .career-axis {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  margin: 0 0 0.9rem;
  padding-left: 1.75rem;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--subtle);
}

.career-axis span { padding-right: 0.4rem; border-right: 1px solid var(--line); text-align: right; line-height: 1.6; }

.career-bar { display: none; }

.js .career-bar {
  position: relative;
  display: block;
  grid-column: 1 / -1;
  grid-row: 3;
  height: 6px;
  margin-top: 0.35rem;
  border-radius: var(--r-pill);
  background:
    repeating-linear-gradient(90deg, transparent 0 calc(20% - 1px), var(--line) calc(20% - 1px) 20%),
    var(--surface-1);
}

.js .career-bar > span {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--a, 0) * 100%);
  width: calc((var(--b, 1) - var(--a, 0)) * 100%);
  border-radius: inherit;
  background: var(--line-hover);
  transform-origin: left center;
}

.career-step.now .career-bar > span { background: linear-gradient(90deg, var(--verdigris), var(--accent)); }

@media (prefers-reduced-motion: no-preference) {
  .reveal-live .career-bar > span { scale: 0 1; transition: scale 1.1s var(--ease); transition-delay: calc(var(--bi, 0) * 160ms + 600ms); }
  .reveal-live .career.in .career-bar > span { scale: none; }
}

@media (max-width: 30rem) {
  .js .career-bar { grid-row: auto; order: 5; }
}

/* The portrait keeps a slow rise against the scroll, so it sits in depth
   beside the text rather than printed onto the page. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .founder-portrait img {
      animation: portrait-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}

@keyframes portrait-drift { from { object-position: 50% 5%; } to { object-position: 50% 25%; } }

/* ── Start a project: what happens next ──────────────────────────────────── */
/* The three things that follow the send button, each one already promised
   elsewhere on the page - one inbox read by the person who does the work, a
   reply within two working days, a fixed quote before code - set as a short
   route down the side of the form. */

.next { margin-top: 1.75rem; }
.next-head { margin: 0 0 0.9rem; font-size: 0.75rem; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--subtle); }

.next-steps { position: relative; display: grid; margin: 0; padding: 0; list-style: none; }

.next-steps::before {
  content: "";
  position: absolute;
  top: 1.2rem;
  bottom: 1.2rem;
  left: calc(1.9rem / 2 - 0.5px);
  width: 1px;
  background: linear-gradient(var(--accent), var(--line-strong));
  transform-origin: top center;
}

.next-steps li { position: relative; display: grid; grid-template-columns: 1.9rem minmax(0, 1fr); gap: 0.9rem; padding: 0.55rem 0; }

.next-num {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--paper);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.next-steps li:first-child .next-num { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
.next-text { font-size: 0.8125rem; line-height: 1.5; color: var(--muted); }
.next-text strong { display: block; font-size: 0.875rem; font-weight: 500; color: var(--ink); }

@media (prefers-reduced-motion: no-preference) {
  .reveal-live .contact-aside .next-steps::before { transform: scaleY(0); transition: transform 1.1s var(--ease) 0.5s; }
  .reveal-live .contact-aside.in .next-steps::before { transform: none; }
}

/* The form's three required answers as three segments above the send
   button, each filling in verdigris the moment its answer is valid. */
.form-steps { display: none; }

.js .form-steps {
  display: flex;
  flex-basis: 100%;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.form-steps li { position: relative; flex: 1; padding-top: 0.6rem; font-size: 0.75rem; color: var(--subtle); transition: color var(--t-mid) var(--ease); }

.form-steps li::before,
.form-steps li::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  border-radius: var(--r-pill);
  background: var(--line-strong);
}

.form-steps li::after { background: var(--verdigris); transform: scaleX(0); transform-origin: left center; transition: transform 0.55s var(--ease); }
.form-steps li.done { color: var(--ink); }
.form-steps li.done::after { transform: none; }

/* Ready to go: the send button's arrow takes the same verdigris. */
.submit-arrow { transition: background var(--t-mid) var(--ease), color var(--t-mid) var(--ease); }
.enquiry.ready .submit-arrow { background: var(--verdigris); color: var(--on-ink); }

/* The message grows with what is written, where the browser can size a
   field to its content. */
.field textarea { field-sizing: content; max-height: 22rem; }

.chip { transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); }
.chip:active { transform: scale(0.96); }

@media (prefers-reduced-motion: reduce) {
  .form-steps li::after { transition: none; }
  .chip:active { transform: none; }
}

/* ── Documents ───────────────────────────────────────────────────────────── */
/* The terms, the privacy notice, the mentions légales and the case study,
   read as documents. site.js adds the parts named doc-*: the reading line
   under the title, the contents rail beside the text on a wide screen (or
   folded under the title on a narrower one, for a long document), a link
   beside each section heading, and the arrival of each section. Every
   section opens on a hairline, so a long page reads in chapters. */

.legal { position: relative; }

.legal > h2 {
  position: relative;
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
}

.doc-meta { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 2.25rem; max-width: none; }

.doc-meta span {
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* The link beside a heading, in the margin, shown when the heading is
   pointed at or the link has focus. */
.doc-anchor {
  position: absolute;
  top: 1.7rem;
  left: -2.9rem;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--r-sm);
  font-weight: 400;
  color: var(--subtle);
  text-decoration: none;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}

.legal > h2:hover .doc-anchor,
.doc-anchor:focus-visible { opacity: 1; }
.doc-anchor:hover { color: var(--ink); }

@media (max-width: 60rem) {
  .doc-anchor { display: none; }
}

/* The contents, folded under the title on anything narrower than the rail
   needs. */
.doc-toc-mobile { margin: -1rem 0 2.25rem; border: 1px solid var(--line); border-radius: var(--r-md); }

.doc-toc-mobile summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 2.75rem;
  padding: 0 1rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}

.doc-toc-mobile summary::-webkit-details-marker { display: none; }
.doc-toc-mobile summary::after { content: "+"; font-weight: 400; color: var(--subtle); transition: transform var(--t-mid) var(--ease); }
.doc-toc-mobile[open] summary::after { transform: rotate(45deg); }
.doc-toc-mobile ol { margin: 0; padding: 0 1rem 0.75rem; list-style: none; border-top: 1px solid var(--line); }
.doc-toc-mobile a { display: flex; align-items: center; min-height: 2.5rem; font-size: 0.875rem; color: var(--muted); text-decoration: none; }
.doc-toc-mobile a:hover { color: var(--ink); }

/* The rail. It stands in the margin to the left of the text, and its list
   stays in view while the document scrolls. */
.doc-toc { display: none; }

@media (min-width: 86rem) {
  .doc-toc-mobile { display: none; }

  .doc-toc {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 100%;
    display: block;
    width: 14rem;
    padding-top: clamp(6rem, 11vw, 9rem);
  }

  .doc-toc-inner { position: sticky; top: 7.5rem; }
  .doc-toc-head { margin: 0 0 0.9rem; font-size: 0.75rem; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--subtle); }
  .doc-toc ol { display: grid; margin: 0; padding: 0; list-style: none; border-left: 1px solid var(--line); }

  .doc-toc a {
    display: flex;
    align-items: center;
    min-height: 2.5rem;
    margin-left: -1px;
    padding: 0.35rem 0 0.35rem 1rem;
    border-left: 1px solid transparent;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--subtle);
    text-decoration: none;
    transition: color var(--t-fast) var(--ease), border-color var(--t-mid) var(--ease), padding var(--t-mid) var(--ease);
  }

  .doc-toc a:hover { color: var(--ink); }
  .doc-toc a.current { padding-left: 1.25rem; border-left-color: var(--accent); color: var(--ink); }
}

@media (prefers-reduced-motion: no-preference) {
  .reveal-live .doc-rise { opacity: 0; translate: 0 14px; transition: opacity 0.7s var(--ease), translate 0.8s var(--ease); }
  .reveal-live .doc-rise.in { opacity: 1; translate: none; }
}

/* Records answer the pointer row by row. */
dl.record > div,
.project-record.wide > div { transition: background var(--t-fast) var(--ease); }
dl.record > div:hover { background: var(--surface-1); }

/* ── The profile, answering ──────────────────────────────────────────────── */
/* The profile is the page a prospect sends to their partner, so it stays a
   document; what it gains is response. The portrait lifts, and keeps a slow
   drift against the scroll. Each fact draws a rubric rule across its top
   when pointed at. A career row is marked down its left edge in cobalt, and
   each principle's number takes the rubric. */
.prof-portrait { transition: transform var(--t-mid) var(--ease), box-shadow var(--t-mid) var(--ease); }
.prof-portrait:hover { transform: translateY(-4px); box-shadow: 0 30px 60px -30px var(--shadow-strong); }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .prof-portrait img {
      animation: portrait-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}

.fact-rail > div { position: relative; }

.fact-rail > div::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: 2.4rem;
  height: 2px;
  background: var(--rubric);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t-mid) var(--ease);
}

.fact-rail > div:hover::before { transform: none; }

.cv-row { position: relative; }

.cv-row::before {
  content: "";
  position: absolute;
  top: 0.9rem;
  bottom: 0.9rem;
  left: -1px;
  width: 2px;
  border-radius: var(--r-pill);
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform var(--t-mid) var(--ease);
}

.cv-row:hover::before { transform: none; }
.cv-row:hover .cv-org { color: var(--ink); }

.principle-num { transition: color var(--t-mid) var(--ease); }
.principle:hover .principle-num { color: var(--rubric); }

@media (prefers-reduced-motion: reduce) {
  .prof-portrait:hover { transform: none; }
  .fact-rail > div::before, .cv-row::before { transition: none; }
}

/* ── The motion layer ────────────────────────────────────────────────────── */
/* Everything below is enhancement, and every piece of it switches off the
   same two ways: a visitor who asked for less motion never has it applied
   (site.js does not split, clone or listen), and a visitor with no JavaScript
   never has the hooks it keys off. Nothing here hides content that the page
   would otherwise show. Transform, opacity and clip-path only. */

/* Display headings arrive a line at a time, each line rising out of its own
   mask. site.js wraps each authored line - split on the <br> already in the
   markup, so the lines are the ones the copy was written in - and adds .in
   when the heading reaches the reader. The mask is padded so descenders and
   French capitals' accents are never clipped. */
.line {
  display: block;
  overflow: hidden;
  padding: 0.08em 0 0.16em;
  margin: -0.08em 0 -0.16em;
}
.line-inner { display: block; }

.js [data-split] .line-inner {
  transform: translateY(108%);
  transition: transform 1.05s var(--ease);
  transition-delay: calc(var(--ln, 0) * 95ms + 60ms);
}
.js [data-split].in .line-inner { transform: none; }

/* Buttons roll their label: the words leave upward and the same words arrive
   from below. The second copy is aria-hidden and added by site.js, so a
   screen reader hears the label once. */
.roll {
  display: inline-grid;
  overflow: hidden;
  padding-block: 0.2em;
  margin-block: -0.2em;
}
.roll > span { grid-area: 1 / 1; transition: transform var(--t-mid) var(--ease); }
.roll-b { transform: translateY(130%); }

.btn:hover .roll-a,
.btn:focus-visible .roll-a,
.pair:hover .roll-a,
.pair:focus-within .roll-a { transform: translateY(-130%); }

.btn:hover .roll-b,
.btn:focus-visible .roll-b,
.pair:hover .roll-b,
.pair:focus-within .roll-b { transform: none; }

/* Reading progress, drawn as light along the lower edge of the bar. It grows
   with background-size, not a transform - the nav's rules in check.js keep
   transforms out of it for Safari's sake - and it is tied to the document's
   scroll by the browser, with no listener. At the top there is no progress
   and so nothing to see. */
.nav-shell::after {
  content: "";
  position: absolute;
  left: 1.75rem;
  right: 1.75rem;
  bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent) 18%) no-repeat 0 0 / 0% 100%;
  pointer-events: none;
}

.nav-shell:has(.nav-links.open)::after { background: none; }

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .nav-shell::after {
      animation: read-progress linear both;
      animation-timeline: scroll(root);
    }
  }
}

@keyframes read-progress { to { background-size: 100% 100%; } }

/* The ticker. What the studio makes, set at display size and moving, as the
   hinge between the work and the way it is made. The first set is a real
   list; the second only closes the loop and is hidden from assistive
   technology. site.js drives it and lets the reader's own scroll speed push
   it along, reversing with them.

   Standing still - no JavaScript, or reduced motion - a single-line band
   would cut the last services off at the edge for good, so the still state
   is the list wrapped onto as many lines as it needs, every item on screen.
   Only once site.js has it moving (.moving) does it become one line that
   runs past both edges. */
.ticker {
  position: relative;
  overflow: hidden;
  padding: clamp(2.25rem, 5vw, 4rem) var(--gutter);
  border-top: 1px solid var(--line);
}

.ticker.moving {
  padding-inline: 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}

.ticker-track { display: flex; justify-content: center; }
.ticker.moving .ticker-track { justify-content: flex-start; width: max-content; will-change: transform; }

.ticker-set {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ticker-set + .ticker-set { display: none; }

/* Still, a long service may take two lines on a narrow screen rather than
   run off it. */
.ticker:not(.moving) .ticker-set { gap: 0.25rem 0; }
.ticker:not(.moving) .ticker-set li { white-space: normal; text-align: center; }

.ticker.moving .ticker-set { flex-wrap: nowrap; flex-shrink: 0; }
.ticker.moving .ticker-set + .ticker-set { display: flex; }

.ticker-set li {
  display: flex;
  align-items: center;
  font-size: clamp(2.25rem, 6.2vw, 5.75rem);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.045em;
  white-space: nowrap;
  color: var(--ink);
}

/* Every other service outlined, so the band reads as a rhythm rather than a
   sentence. */
.ticker-set li:nth-child(even) {
  color: transparent;
  -webkit-text-stroke: 1px var(--muted);
}

/* The mark between them, in the cobalt, taking its shape from the one mask. */
.ticker-set li::after {
  content: "";
  flex: none;
  width: 0.4em;
  height: 0.48em;
  margin-inline: 0.6em;
  background: var(--accent);
  -webkit-mask: url('assets/canon-fidei-mark.svg') no-repeat center / contain;
  mask: url('assets/canon-fidei-mark.svg') no-repeat center / contain;
}

/* Over a project window with a mouse, the pointer carries its destination:
   a pill that says the window is a link and where it goes. The window was
   already the link; this only says so. */
.proj-cursor {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 0.95rem;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--on-ink);
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  scale: 0.5;
  translate: var(--cx, 0px) var(--cy, 0px);
  box-shadow: 0 10px 30px -8px var(--shadow-strong);
  transition: opacity 240ms var(--ease), scale 360ms var(--ease);
}

.proj-cursor svg { width: 0.7rem; height: 0.7rem; }
.proj-window:hover .proj-cursor { opacity: 1; scale: 1; }

/* A project window opens out of its own frame as it arrives, from an inset,
   rounded aperture to the full window. The resting inset is negative so the
   window's shadow and focus ring are never clipped. */
@media (prefers-reduced-motion: no-preference) {
  .js .proj .proj-window {
    clip-path: inset(7% 7% 7% 7% round var(--r-lg));
    transition: clip-path 1.25s var(--ease) 80ms,
                border-color var(--t-mid) var(--ease),
                box-shadow var(--t-mid) var(--ease);
  }
  .js .proj.in .proj-window { clip-path: inset(-6rem round 0); }
}

/* The signature, lit. On the home page the name across the foot is set in
   the faintest ink, and a cobalt light follows a mouse pointer across its
   letters (site.js moves --wx and --wy). Where the browser cannot clip a
   background to text, or mix a colour, the plain faint wordmark stays. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) and (color: color-mix(in srgb, red 50%, transparent)) {
  .wordmark[data-spot] {
    opacity: 1;
    color: transparent;
    background:
      radial-gradient(24rem circle at var(--wx, 50%) var(--wy, -60rem), var(--accent), transparent 70%),
      linear-gradient(color-mix(in srgb, var(--ink) 7%, transparent), color-mix(in srgb, var(--ink) 7%, transparent));
    -webkit-background-clip: text;
    background-clip: text;
  }
}

/* The footer's signature rises into place as the page ends. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .wordmark {
      animation: wordmark-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 100%;
    }
  }
}

@keyframes wordmark-rise { from { opacity: 0; transform: translateY(40%); } }

.legal-lines { min-width: 0; }

/* The manifesto follows the experience strip closely: it is the sentence
   that strip introduces, not a new chapter. */
.manifesto-band { padding-top: clamp(3rem, 6vw, 5rem); }
