:root{
  --ink: #14130F;
  --ink-2: #1B1A17;
  --ink-3: #221F19;
  --paper: #F4F5F3;
  --paper-dim: #E9E4D9;
  --gold-1: #FFF3C4;
  --gold-2: #FFD54B;
  --gold-3: #F5C243;
  --gold-4: #C98A2B;
  --gold-5: #8A5A12;
  --gold-6: #4A2F09;
  --text-dim: #A79E8C;
  --text-dim-2: #8E8676;
  --line: rgba(233,228,217,0.12);
  --line-strong: rgba(233,228,217,0.18);
  /* Form control edges. --line is a divider, 1.4:1 — too faint to show where
     a field is. A control's boundary has to reach 3:1 (WCAG 1.4.11) against
     both its own fill (--ink-2) and the page: 3.3:1 and 3.5:1 here. */
  --field-edge: #716B60;
  /* Gold has to do two contradictory jobs: it is bright text on a dark page,
     and it is the surface of the gold button. One value can serve both only
     while the page is dark. These split the text role out so light mode can
     darken the lettering without turning the button to mud. Dark values are
     exactly the golds they replace, so dark mode is unchanged. */
  --gold-ink: #FFD54B;
  --gold-ink-2: #F5C243;
  --gold-edge: #F5C243;
  /* Ink on a gold surface. Gold stays gold in both themes, so this stays dark
     in both — it cannot follow --ink, which inverts. */
  --on-gold: #1B1204;
  --header-bg: rgba(20,19,15,0.72);
  --header-bg-scrolled: rgba(20,19,15,0.88);
  --shadow-1: rgba(0,0,0,0.55);
  --shadow-2: rgba(0,0,0,0.9);

  /* ---- Effect layer ----
     Every gold that is painted as an *effect* rather than as text: the card
     sweep, the pointer glow, the tint chips, the accent edges, and the four
     gradients. These were literals until light mode needed them; the dark
     values below are byte-for-byte the literals they replace, so dark mode
     renders exactly as before.

     They exist because a gold tuned to glow on near-black does nothing on
     paper — rgba(245,194,67,0.12) over #FFFDF7 is a 1.02 shift, i.e. nothing.
     Light mode re-specifies each one as a *darkening* of the page instead of
     a lightening, which is the only way an effect reads on a pale surface. */

  /* Marks that are drawn rather than written: dots, nodes, list bullets.
     Not text, so the bar is 3:1 — but they carry meaning, so they get it. */
  --gold-mark: #F5C243;

  /* Flat tints used as chip/panel fills. */
  --tint-faint: rgba(245,194,67,0.045);
  --tint-soft: rgba(245,194,67,0.10);
  --tint-mid: rgba(245,194,67,0.12);

  /* Accent borders at three strengths: scrolled header, open FAQ, sent panel. */
  --edge-soft: rgba(245,194,67,0.18);
  --edge-mid: rgba(245,194,67,0.28);
  --edge-strong: rgba(245,194,67,0.35);

  /* Motion effects. --sweep is the diagonal band that crosses a card face;
     --cursor-glow follows the pointer; --glow-soft is the work-card corner
     wash; --glow-dot is the bloom on the why-item dot; --hero-bloom is the
     soft light in the hero field. */
  --sweep: rgba(255,243,196,0.09);
  --cursor-glow: rgba(245,194,67,0.13);
  --glow-soft: rgba(245,194,67,0.12);
  --glow-dot: rgba(245,194,67,0.6);
  /* The lit node on the process connecting line. */
  --glow-node: rgba(245,194,67,0.40);
  --hero-bloom: rgba(245,194,67,0.05);

  /* Gradients. Each mixes a bright stop that only works over a dark field,
     so each needs a light counterpart rather than a per-stop swap. */
  --grad-head: linear-gradient(100deg, var(--gold-4), var(--gold-2) 50%, var(--gold-1));
  --grad-progress: linear-gradient(90deg, var(--gold-5), var(--gold-2) 60%, var(--gold-1));
  --grad-underline: linear-gradient(90deg, var(--gold-4), var(--gold-2));
  --grad-shard: linear-gradient(140deg, var(--gold-1), var(--gold-3) 55%, var(--gold-4));
  --grad-step-line: linear-gradient(180deg, var(--gold-4), var(--gold-2) 45%, var(--gold-4) 85%, transparent);

  /* The active tab is the one control that changes *kind* between themes:
     gold pill on the dark page, near-black pill on the light one. See the
     light block for why. */
  --tab-on-bg: linear-gradient(100deg,var(--gold-4),var(--gold-2));
  --tab-on-fg: var(--on-gold);
  --tab-on-edge: var(--gold-3);

  /* The lit node on process.html's connecting line. Dark is --gold-2; on a
     pale page that is 1.5:1, so light takes the deeper mark colour. */
  --node-lit: var(--gold-2);

  /* Category glyphs on work.html — see the note at .cat-visual there. */
  --glyph-ink: #F5C243;
  --glyph-opacity: 0.5;

  /* Grain strength. A grey noise plate darkens a light page more readily
     than it lightens a dark one, so the two are not the same number. */
  --grain-opacity: 0.038;

  --maxw: 1180px;
  color-scheme: dark;
}
*{ margin:0; padding:0; box-sizing:border-box; }
@media (prefers-reduced-motion: no-preference){
  html{ scroll-behavior:smooth; }
}
/* fixed header is 76px — keep in-page anchors clear of it. Focus too: tabbing
   backwards scrolls each control to the top edge, under the header. */
:target, :focus{ scroll-margin-top:96px; }
body{
  background:var(--ink);
  color:var(--paper);
  font-family:'Poppins', sans-serif;
  font-weight:400;
  line-height:1.5;
  overflow-x:hidden;
}
a{ color:inherit; text-decoration:none; }
:focus-visible{
  outline:2px solid var(--gold-edge); outline-offset:3px; border-radius:4px;
}
.skip-link{
  position:absolute; left:16px; top:-60px; z-index:300;
  background:var(--gold-2); color:var(--on-gold); padding:12px 20px;
  border-radius:0 0 8px 8px; font-weight:600; font-size:14px;
  transition:top .2s ease;
}
.skip-link:focus{ top:0; }
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
img,svg{ display:block; max-width:100%; }
.wrap{ max-width:var(--maxw); margin:0 auto; padding-left:32px; padding-right:32px; }
::selection{ background:var(--gold-3); color:var(--on-gold); }

h1,h2,h3{ font-weight:700; letter-spacing:-0.01em; }
.eyebrow{
  font-size:13px; font-weight:600; letter-spacing:0.08em;
  color:var(--gold-ink-2); margin-bottom:14px; display:inline-block;
}

/* ---------- Buttons ---------- */
.btn{
  position:relative; display:inline-flex; align-items:center; justify-content:center;
  padding:15px 30px; border-radius:8px; font-weight:600; font-size:15px;
  cursor:pointer; border:none; overflow:hidden; isolation:isolate;
  transition:transform .25s ease;
}
.btn:hover{ transform:translateY(-2px); }
.btn-primary{
  background:linear-gradient(115deg, var(--gold-4), var(--gold-2) 45%, var(--gold-1));
  color:var(--on-gold);
}
.btn-primary::before{
  content:''; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.55) 50%, transparent 70%);
  transform:translateX(-120%); transition:transform .6s ease;
}
.btn-primary:hover::before{ transform:translateX(120%); }
.btn-ghost{
  background:transparent; color:var(--paper); border:1.5px solid var(--line);
}
.btn-ghost:hover{ border-color:var(--gold-edge); color:var(--gold-ink); }

/* ---------- Header ---------- */
header{
  position:fixed; top:0; left:0; right:0; z-index:100;
  background:var(--header-bg); backdrop-filter:blur(14px);
  border-bottom:1px solid var(--line);
}
.nav{ display:flex; align-items:center; justify-content:space-between; height:76px; }
.brand{ display:flex; align-items:center; gap:10px; }
.brand svg{ width:34px; height:34px; }
.brand span{ font-weight:700; font-size:19px; letter-spacing:-0.01em; }
nav.links{ display:flex; gap:36px; }
nav.links a{ font-size:14.5px; font-weight:500; color:var(--text-dim); transition:color .2s; }
nav.links a:hover, nav.links a.current{ color:var(--paper); }
.nav-cta{ display:flex; align-items:center; gap:24px; }
.menu-btn{ display:none; background:none; border:none; color:var(--paper); font-size:24px; cursor:pointer; }
/* Scrolls when it is taller than the screen (a phone on its side). Centring
   is done by the two flexible spacers rather than justify-content:center,
   which pushes overflowing items off the top where no scroll can reach. */
.mobile-menu{
  position:fixed; inset:0; background:var(--ink); z-index:200;
  display:flex; flex-direction:column; align-items:center; gap:30px;
  padding:72px 24px 32px; overflow-y:auto; overscroll-behavior:contain;
  transform:translateY(-100%); transition:transform .4s ease, visibility .4s;
  visibility:hidden;
}
.mobile-menu::before, .mobile-menu::after{ content:''; flex:1 0 0; }
.mobile-menu > *{ flex-shrink:0; }
/* Visible at once on open (visibility would otherwise stay hidden for the
   first frame, and the focus main.js moves into the menu would be refused);
   the base rule's delay still keeps it visible while it slides away. */
.mobile-menu.open{ transform:translateY(0); visibility:visible; transition:transform .4s ease, visibility 0s; }
/* top is set inline by main.js to the negative scroll offset */
body.menu-open{ position:fixed; left:0; right:0; width:100%; overflow:hidden; }
.mobile-menu a:not(.btn){ font-size:23px; font-weight:600; color:var(--paper); }
.mobile-menu .btn{ font-size:23px; }
/* 44px target; the glyph stays where it was drawn (top 26, right 28). */
.menu-close{
  position:absolute; top:18px; right:15px; min-width:44px; min-height:44px;
  display:inline-flex; align-items:center; justify-content:center;
  background:none; border:none; color:var(--paper); font-size:24px; cursor:pointer;
}

/* ---------- Page hero (interior pages) ---------- */
.page-hero{ padding-top:170px; padding-bottom:80px; }
.page-hero h1{ font-size:46px; line-height:1.1; max-width:760px; margin-bottom:20px; }
.page-hero h1 em{
  font-style:normal;
  background:var(--grad-head);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
.page-hero p{ font-size:18px; color:var(--text-dim); max-width:600px; }
.breadcrumb{ font-size:13.5px; color:var(--text-dim-2); margin-bottom:22px; }
.breadcrumb a{ color:var(--text-dim); display:inline-block; padding:3px 0; }
.breadcrumb a:hover{ color:var(--gold-ink-2); }

/* angled divider
   The SVG polygon inherits this colour, so the divider follows the theme.
   It must stay equal to --ink-2, the band it is the top edge of: anything
   else shows up as a hard stripe rather than an angled join. */
.facet-divider{ position:relative; height:70px; margin-top:-1px; color:var(--ink-2); }
.facet-divider svg{ width:100%; height:100%; display:block; fill:currentColor; }

/* ---------- Section shell ---------- */
section{ position:relative; padding:100px 0; }
.section-head{ max-width:640px; margin-bottom:60px; }
.section-head h2{ font-size:36px; margin-bottom:16px; }
.section-head p{ font-size:17px; color:var(--text-dim); }
.alt-bg{ background:var(--ink-2); }

/* Hiding is gated on .js (set by the inline script in every page's head).
   Without JS nothing adds .visible, so without JS nothing may hide either —
   the content simply renders, unanimated.

   The gate is :where(.js), not .js, because :where() contributes no
   specificity. A plain `.js .reveal` would outweigh the per-card transition
   rules further down (.teaser-card, .svc-card, .work-card), which currently
   win on source order — cards would start fading instead of snapping in.
   :where() keeps every one of these rules at exactly its original weight. */
:where(.js) .reveal{ opacity:0; transform:translateY(18px); transition:opacity .6s ease, transform .6s ease; }
:where(.js) .reveal.visible{ opacity:1; transform:translateY(0); }
@media (prefers-reduced-motion: reduce){
  :where(.js) .reveal{ opacity:1; transform:none; transition:none; }
  *, *::before, *::after{ animation-duration:.01ms !important; transition-duration:.01ms !important; }
}

/* ---------- Tabs & service cards ---------- */
.tabs{ display:flex; gap:10px; margin-bottom:48px; flex-wrap:wrap; }
.tab{
  padding:11px 20px; border-radius:999px; border:1.5px solid var(--line);
  font-size:14px; font-weight:600; color:var(--text-dim); cursor:pointer; background:transparent;
  transition:all .25s ease;
}
.tab.active{ border-color:var(--tab-on-edge); color:var(--tab-on-fg); background:var(--tab-on-bg); }
.tab-panels{ position:relative; }
/* The grid is the panel's own layout, not something the "active" state confers,
   so a visitor without JS gets all three panels laid out identically and every
   service is reachable. Switching — hiding the panels that aren't current — is
   the part JS adds, so that part is gated on .js. :where() contributes no
   specificity, keeping these rules at exactly their original weight. */
.tab-panel{ display:grid; grid-template-columns:repeat(2, 1fr); gap:20px; }
:where(.js) .tab-panel{ display:none; }
:where(.js) .tab-panel.active{ display:grid; }

.svc-card{
  background:var(--ink-3); border:1px solid var(--line); padding:28px 26px;
  clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 18px 100%, 0 calc(100% - 18px));
  transition:border-color .25s ease, transform .25s ease;
}
.svc-card:hover{ border-color:var(--gold-4); transform:translateY(-3px); }
/* Card arrived at via a deep link (e.g. from work.html) keeps the hover border. */
.svc-card:target{ border-color:var(--gold-4); }
.svc-card h3{ font-size:18px; margin-bottom:10px; color:var(--paper); }
.svc-card p{ font-size:14.5px; color:var(--text-dim); }
.svc-card ul{ margin-top:14px; padding-top:14px; border-top:1px solid var(--line); list-style:none; }
.svc-card ul li{ font-size:13.5px; color:var(--paper-dim); padding:5px 0 5px 20px; position:relative; }
.svc-card ul li::before{ content:'✓'; position:absolute; left:0; color:var(--gold-ink); font-weight:700; }
.svc-num{
  display:inline-flex; align-items:center; justify-content:center;
  width:30px; height:30px; border-radius:7px; font-size:13px; font-weight:700;
  background:var(--tint-mid); color:var(--gold-ink); margin-bottom:16px;
}

/* ---------- Process ---------- */
.process{ display:grid; grid-template-columns:repeat(4,1fr); gap:24px; counter-reset:step; }
.step{ position:relative; padding-top:20px; }
.step::before{
  counter-increment:step; content:counter(step, decimal-leading-zero);
  font-size:15px; font-weight:700; color:var(--gold-ink-2);
  display:block; margin-bottom:18px; letter-spacing:0.05em;
}
.step h3{ font-size:19px; margin-bottom:10px; }
.step p{ font-size:14.5px; color:var(--text-dim); }

/* ---------- Why / feature grid ---------- */
.why-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:2px; background:var(--line); border:1px solid var(--line); }
.why-item{ background:var(--ink); padding:44px 40px; }
.why-item h3{ font-size:20px; margin-bottom:12px; display:flex; align-items:center; gap:12px; }
.why-item h3 .dot{ width:8px; height:8px; border-radius:2px; background:var(--gold-mark); transform:rotate(45deg); flex-shrink:0; }
.why-item p{ color:var(--text-dim); font-size:15px; }

/* ---------- Work / capability categories ---------- */
.work-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:20px; }
.work-card{
  aspect-ratio:1/1.1; border-radius:14px; padding:26px; position:relative; overflow:hidden;
  display:flex; flex-direction:column; justify-content:flex-end;
  border:1px solid var(--line);
  background:linear-gradient(160deg, var(--ink-3), var(--ink-2));
}
.work-card::before{
  content:''; position:absolute; top:-40%; right:-40%; width:140%; height:140%;
  background:radial-gradient(circle, var(--glow-soft), transparent 60%);
  opacity:0; transition:opacity .35s ease;
}
.work-card:hover::before{ opacity:1; }
/* The card is taller than its copy, and the copy is pinned to the bottom, so
   the top half was simply empty. On the dark page that reads as negative
   space; on a light one it reads as an unfinished card. The category mark
   fills it — the same glyph work.html uses for the same category, so the two
   pages agree. margin-bottom:auto is what pushes it to the top of the flex
   column; position:relative keeps it above the corner glow, as h3/p do. */
.work-card .work-glyph{
  width:34px; height:34px; margin-bottom:auto; position:relative;
  color:var(--glyph-ink); opacity:var(--glyph-opacity);
  transition:opacity .3s ease, transform .35s cubic-bezier(.22,.68,.32,1);
}
.work-card:hover .work-glyph{ opacity:1; transform:scale(1.08); }
.work-card h3{ font-size:17px; margin-bottom:8px; position:relative; }
.work-card p{ font-size:13.5px; color:var(--text-dim); position:relative; }

/* ---------- Contact ---------- */
.contact-grid{ display:grid; grid-template-columns:0.9fr 1.1fr; gap:60px; }
.contact-info p{ color:var(--text-dim); font-size:16px; margin-bottom:34px; max-width:400px; }
.contact-line{ display:flex; align-items:center; gap:12px; font-size:15px; margin-bottom:16px; color:var(--paper-dim); }
.contact-line .ic{ color:var(--gold-ink-2); font-weight:700; }
.contact-line a{ color:inherit; padding:3px 0; display:inline-block; }
.contact-line a:hover{ color:var(--gold-ink-2); }

form{ display:grid; gap:18px; }
.form-row{ display:grid; grid-template-columns:1fr 1fr; gap:18px; }
label{ font-size:13px; color:var(--text-dim); margin-bottom:7px; display:block; }
input, select, textarea{
  width:100%; background:var(--ink-2); border:1px solid var(--field-edge); border-radius:8px;
  padding:13px 14px; color:var(--paper); font-family:inherit; font-size:14.5px;
  transition:border-color .2s;
}
/* The radius is restated because the global :focus-visible rule (4px)
   outranks the element selector above, and fields would square off. */
input:focus, select:focus, textarea:focus{ border-color:var(--gold-edge); border-radius:8px; }
textarea{ min-height:120px; resize:vertical; }

/* ---------- Custom select (Project type) ----------
   A progressive enhancement over the native <select>, not a replacement:
   main.js keeps the real control in the DOM and writes every change back
   to it, so the native POST still carries the value and a visitor without
   JS just gets the plain browser select. Nothing here is load-bearing for
   submission. */
.sel{ position:relative; }
.sel-native{ display:none; }

.sel-btn{
  width:100%; display:flex; align-items:center; justify-content:space-between; gap:12px;
  background:var(--ink-2); border:1px solid var(--field-edge); border-radius:8px;
  padding:13px 14px; color:var(--paper); font-family:inherit; font-size:14.5px;
  text-align:left; cursor:pointer;
  position:relative; overflow:hidden; isolation:isolate;
  transition:border-color .2s ease;
}
/* Same negative-z sweep as .btn-primary, dialled down to a gold tint so it
   reads as a field rather than a call to action. */
.sel-btn::before{
  content:''; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(115deg, transparent 30%, var(--tint-soft) 50%, transparent 70%);
  transform:translateX(-120%); transition:transform .6s ease;
}
.sel-btn:hover::before{ transform:translateX(120%); }
.sel-btn:hover{ border-color:var(--gold-4); }
.sel[data-open="true"] .sel-btn{ border-color:var(--gold-edge); }

.sel-caret{
  width:8px; height:8px; flex-shrink:0; margin-right:3px;
  border-right:2px solid var(--gold-edge); border-bottom:2px solid var(--gold-edge);
  transform:translateY(-2px) rotate(45deg);
  transition:transform .3s ease;
}
.sel[data-open="true"] .sel-caret{ transform:translateY(2px) rotate(-135deg); }

.sel-list{
  position:absolute; z-index:40; top:calc(100% + 6px); left:0; right:0;
  list-style:none; margin:0; padding:5px;
  background:var(--ink-3); border:1px solid var(--gold-5); border-radius:8px;
  box-shadow:0 18px 40px var(--shadow-1);
  max-height:260px; overflow-y:auto;
  opacity:0; transform:translateY(-6px); pointer-events:none; visibility:hidden;
  transition:opacity .2s ease, transform .2s ease, visibility 0s linear .2s;
}
.sel[data-open="true"] .sel-list{
  opacity:1; transform:translateY(0); pointer-events:auto; visibility:visible;
  transition:opacity .2s ease, transform .2s ease, visibility 0s;
}

.sel-opt{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:11px 12px; border-radius:6px;
  font-size:14.5px; color:var(--paper-dim); cursor:pointer;
  transition:background-color .15s ease, color .15s ease;
}
/* Hover and keyboard cursor are the same highlight on purpose — there is
   only ever one active option, whichever input moved it. */
.sel-opt[data-active="true"]{ background:var(--tint-mid); color:var(--paper); }
.sel-opt[aria-selected="true"]{ color:var(--gold-ink); font-weight:600; }
.sel-opt[aria-selected="true"]::after{ content:'\2713'; font-size:12px; color:var(--gold-ink); }

/* ---------- Meeting request (contact form) ----------
   A checkbox plus two follow-up fields. The fields are authored visible and
   collapsed by main.js, so with JS off they are just part of the form — the
   same arrangement the custom select uses, and for the same reason: nothing
   here is load bearing for submission.

   The box is a real checkbox with appearance stripped, not a styled span, so
   it keeps its own keyboard behaviour and focus ring. The tick is painted as
   a background image rather than a pseudo-element, which inputs don't render
   reliably. */
.meet{ display:grid; gap:18px; }
.check{ display:flex; align-items:flex-start; gap:11px; }
.check input[type="checkbox"]{
  appearance:none; -webkit-appearance:none;
  width:18px; height:18px; flex:0 0 18px; margin:1px 0 0; padding:0;
  background-color:var(--ink-2); border:1px solid var(--field-edge); border-radius:5px;
  cursor:pointer; transition:background-color .2s, border-color .2s;
}
.check input[type="checkbox"]:hover{ border-color:var(--gold-4); }
.check input[type="checkbox"]:checked{
  background-color:var(--gold-2); border-color:var(--gold-edge);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23141310' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.4l3 3 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:center; background-size:12px 12px;
}
/* The label is the click target for the box, so it gets the pointer too, and
   it reads as a sentence rather than a field name — no bottom margin. */
.check label{
  margin-bottom:0; font-size:14px; line-height:1.45;
  color:var(--paper-dim); cursor:pointer;
}
.check input[type="checkbox"]:checked + label{ color:var(--paper); }
/* display:grid here is what [hidden] has to beat, hence the explicit rule. */
.meet-fields{ display:grid; gap:18px; }
.meet-fields[hidden]{ display:none; }
/* Only worth animating where JS is what reveals it. Without JS the block is
   simply present from the start and must not animate on load. :where() keeps
   the zero-specificity gate used everywhere else in this file. */
:where(.js) .meet-fields{ animation:meet-in .28s cubic-bezier(.22,.68,.32,1) both; }
@keyframes meet-in{
  from{ opacity:0; transform:translateY(-6px); }
  to{ opacity:1; transform:none; }
}

/* Meeting type — native radios in a fieldset, so the grouping and the
   legend-as-group-label come from HTML rather than bolted-on ARIA. Appearance
   is stripped so they read as the checkbox above does; the dot is a radial
   gradient rather than a pseudo-element, which inputs don't render reliably. */
.choice{ border:0; padding:0; margin:0; min-inline-size:0; }
.choice legend{
  padding:0; font-size:13px; color:var(--text-dim); margin-bottom:9px;
}
.choice-opts{ display:flex; flex-wrap:wrap; gap:10px 22px; }
.opt{ display:inline-flex; align-items:center; gap:9px; }
.opt input[type="radio"]{
  appearance:none; -webkit-appearance:none;
  width:18px; height:18px; flex:0 0 18px; margin:0; padding:0;
  background-color:var(--ink-2); border:1px solid var(--field-edge); border-radius:50%;
  cursor:pointer; transition:background-color .2s, border-color .2s;
}
.opt input[type="radio"]:hover{ border-color:var(--gold-4); }
.opt input[type="radio"]:checked{
  border-color:var(--gold-edge); background-color:var(--gold-2);
  background-image:radial-gradient(circle at center, var(--on-gold) 0 3.5px, transparent 3.5px);
}
.opt label{
  margin-bottom:0; font-size:14px; line-height:1.45;
  color:var(--paper-dim); cursor:pointer;
}
.opt input[type="radio"]:checked + label{ color:var(--paper); }
/* The two questions are different lengths, so one label wraps to a second
   line before the other does and the inputs beneath them stop lining up.
   Bottom-align each input within its cell rather than trimming the labels. */
.meet-fields .form-row > div{ display:flex; flex-direction:column; justify-content:space-between; }

/* ---------- FAQ (accordion) ---------- */
.faq-item{ border-top:1px solid var(--line); }
.faq-item:last-child{ border-bottom:1px solid var(--line); }
.faq-q{
  width:100%; text-align:left; background:none; border:none; color:var(--paper);
  font-family:inherit; font-size:16.5px; font-weight:600; padding:24px 0; cursor:pointer;
  display:flex; justify-content:space-between; align-items:center; gap:20px;
}
.faq-q .plus{ font-size:20px; color:var(--gold-ink-2); transition:transform .3s ease; flex-shrink:0; }
.faq-item.open .faq-q .plus{ transform:rotate(45deg); }
/* Collapsed only where JS can open it again. main.js sets an inline
   max-height to expand, and nothing without JS ever could — so without JS the
   answers are simply not collapsed, and stay readable. */
:where(.js) .faq-a{ max-height:0; overflow:hidden; transition:max-height .35s ease; }
.faq-a p{ color:var(--text-dim); font-size:15px; padding-bottom:24px; max-width:700px; }

/* ---------- Simple content blocks (about/story) ---------- */
.two-col{ display:grid; grid-template-columns:1fr 1fr; gap:60px; align-items:start; }
.stat-row{ display:flex; gap:44px; flex-wrap:wrap; margin-top:36px; }
.stat-row .num{ font-size:30px; font-weight:800; color:var(--gold-ink); display:block; }
.stat-row .lbl{ font-size:13.5px; color:var(--text-dim); }
.value-list{ list-style:none; display:grid; gap:22px; }
.value-list li{ padding-left:28px; position:relative; color:var(--paper-dim); font-size:15.5px; }
.value-list li::before{ content:'◆'; position:absolute; left:0; top:1px; color:var(--gold-ink-2); font-size:12px; }

/* ---------- Final CTA band ---------- */
.cta-band{
  background:linear-gradient(135deg, var(--ink-2), var(--ink-3));
  border:1px solid var(--line); border-radius:20px; padding:60px 56px;
  display:flex; align-items:center; justify-content:space-between; gap:40px; flex-wrap:wrap;
}
.cta-band h2{ font-size:30px; margin-bottom:8px; }
.cta-band p{ color:var(--text-dim); font-size:15.5px; }

/* ---------- Footer ---------- */
footer{ border-top:1px solid var(--line); padding:44px 0; }
.foot-row{ display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:20px; }
.foot-row .brand span{ font-size:16px; }
.foot-links{ display:flex; gap:8px 26px; flex-wrap:wrap; justify-content:center; }
.foot-links a{ font-size:13.5px; color:var(--text-dim); padding:5px 0; display:inline-block; }
.foot-note{ font-size:13px; color:var(--text-dim-2); }

@media (max-width: 980px){
  nav.links{ display:none; }
  .nav-cta{ display:none; }
  .menu-btn{
    display:inline-flex; align-items:center; justify-content:center;
    min-width:44px; min-height:44px;
  }
  .page-hero h1{ font-size:34px; }
  .tab-panel{ grid-template-columns:1fr; }
  .process{ grid-template-columns:1fr 1fr; row-gap:36px; }
  .why-grid{ grid-template-columns:1fr; }
  .work-grid{ grid-template-columns:1fr 1fr; }
  .contact-grid{ grid-template-columns:1fr; gap:40px; }
  .form-row{ grid-template-columns:1fr; }
  .two-col{ grid-template-columns:1fr; gap:36px; }
  .cta-band{ padding:40px 28px; flex-direction:column; align-items:flex-start; }
  .wrap{ padding-left:22px; padding-right:22px; }
}

@media (max-width: 600px){
  .work-grid{ grid-template-columns:1fr; }
  /* The 1:1.1 ratio is there to keep four narrow cards square-ish in a row.
     Once they stack one per row it works against them: a full-width card
     becomes ~380px tall for two lines of copy, and the card reads as empty
     again — the same failure the glyph was added to fix. Size to content
     here, and give the glyph its own bottom margin since margin-bottom:auto
     has no free space left to distribute. */
  .work-card{ aspect-ratio:auto; min-height:0; }
  .work-card .work-glyph{ margin-bottom:18px; }
  .process{ grid-template-columns:1fr; }
  .stat-row{ gap:24px; }
  .page-hero{ padding-top:140px; padding-bottom:60px; }
  .page-hero h1{ font-size:29px; }
  .section-head h2{ font-size:27px; }
  .cta-band h2{ font-size:25px; }
  section{ padding:70px 0; }
  .why-item{ padding:32px 24px; }
  .foot-row{ justify-content:center; text-align:center; }
}

/* ============================================================
   Motion layer
   Interaction and scroll behaviour shared by every page.
   Everything here degrades to a static, fully-visible page when
   prefers-reduced-motion is set — see the guard at the bottom.
   ============================================================ */

/* ---------- Scroll progress ---------- */
.scroll-progress{
  position:fixed; top:0; left:0; height:2px; width:100%; z-index:150;
  transform:scaleX(0); transform-origin:0 50%;
  background:var(--grad-progress);
  pointer-events:none;
}

/* ---------- Header condense on scroll ---------- */
header{ transition:background .3s ease, border-color .3s ease, box-shadow .3s ease; }
header.scrolled{
  background:var(--header-bg-scrolled);
  border-bottom-color:var(--edge-soft);
  box-shadow:0 10px 30px -22px var(--shadow-2);
}
nav.links a{ position:relative; }
/* Underline grows from the centre rather than snapping in. */
nav.links a::after{
  content:''; position:absolute; left:0; right:0; bottom:-6px; height:1.5px;
  background:var(--grad-underline);
  transform:scaleX(0); transform-origin:50% 50%;
  transition:transform .3s cubic-bezier(.22,.68,.32,1);
}
nav.links a:hover::after, nav.links a.current::after{ transform:scaleX(1); }

/* ---------- Reveal (staggered, decelerating) ----------
   Same .js gate as the earlier pair: this is the definition that actually
   wins, and it is the one that would otherwise strand a no-JS visitor on a
   blank page. */
:where(.js) .reveal{
  opacity:0; transform:translateY(22px);
  transition:opacity .7s cubic-bezier(.22,.68,.32,1), transform .7s cubic-bezier(.22,.68,.32,1);
  transition-delay:var(--reveal-delay, 0ms);
}
:where(.js) .reveal.visible{ opacity:1; transform:none; }

/* ---------- Card hover: light sweep + pointer glow ----------
   Same isolation/negative-z trick as .btn-primary, so the sweep
   passes over the card face but stays under the text. */
.svc-card, .teaser-card, .work-card, .step, .why-item, .quote-doc{ position:relative; isolation:isolate; }
.svc-card::before, .teaser-card::before, .work-card::after, .quote-doc::before{
  content:''; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:linear-gradient(115deg, transparent 38%, var(--sweep) 50%, transparent 62%);
  transform:translateX(-130%);
  transition:transform .75s cubic-bezier(.22,.68,.32,1);
}
.svc-card:hover::before, .teaser-card:hover::before, .work-card:hover::after, .quote-doc:hover::before{
  transform:translateX(130%);
}
/* Glow follows the cursor across the card face. */
.svc-card::after, .teaser-card::after, .quote-doc::after{
  content:''; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:radial-gradient(320px circle at var(--mx,50%) var(--my,50%), var(--cursor-glow), transparent 60%);
  opacity:0; transition:opacity .35s ease;
}
.svc-card:hover::after, .teaser-card:hover::after, .quote-doc:hover::after{ opacity:1; }
.svc-card{ transition:border-color .25s ease, transform .35s cubic-bezier(.22,.68,.32,1), box-shadow .35s ease; }
.teaser-card{ transition:border-color .25s ease, transform .35s cubic-bezier(.22,.68,.32,1), box-shadow .35s ease; }
.svc-card:hover, .teaser-card:hover{ transform:translateY(-4px); }
.work-card{ transition:transform .35s cubic-bezier(.22,.68,.32,1), border-color .25s ease, box-shadow .35s ease; }
.work-card:hover{ transform:translateY(-5px); border-color:var(--gold-4); }

/* Process steps and why-items get a quieter version: the accent
   marker responds, the card itself stays put. */
.step::before{ transition:color .3s ease, transform .35s cubic-bezier(.22,.68,.32,1); }
.step:hover::before{ color:var(--gold-ink); transform:translateX(4px); }
.why-item{ transition:background .35s ease; }
.why-item:hover{ background:var(--ink-3); }
.why-item h3 .dot{ transition:transform .45s cubic-bezier(.22,.68,.32,1), box-shadow .35s ease; }
.why-item:hover h3 .dot{ transform:rotate(135deg) scale(1.25); box-shadow:0 0 14px var(--glow-dot); }

/* ---------- Interior page hero parallax ---------- */
.page-hero h1, .page-hero > p, .page-hero .breadcrumb{ will-change:transform; }

/* ---------- Tabs ---------- */
.tab{ transition:border-color .25s ease, color .25s ease, background .25s ease, transform .25s ease; }
.tab:hover{ border-color:var(--gold-4); color:var(--paper); }
.tab:active{ transform:scale(.97); }
:where(.js) .tab-panel.active > *{
  animation:panel-in .5s cubic-bezier(.22,.68,.32,1) both;
  animation-delay:var(--card-delay, 0ms);
}
@keyframes panel-in{
  from{ opacity:0; transform:translateY(14px); }
  to{ opacity:1; transform:none; }
}

/* ---------- FAQ ---------- */
.faq-q{ transition:color .25s ease; }
.faq-q:hover{ color:var(--gold-ink); }
.faq-a{ transition:max-height .4s cubic-bezier(.22,.68,.32,1); }
/* The answer text slides down as the panel opens. Deliberately no opacity
   here: if a transition never runs, the text must still be readable. */
:where(.js) .faq-a p{ transform:translateY(-8px); transition:transform .45s cubic-bezier(.22,.68,.32,1); }
.faq-item.open .faq-a p{ transform:none; }
.faq-item{ transition:border-color .3s ease; }
.faq-item.open{ border-top-color:var(--edge-strong); }

/* ---------- Touch targets ----------
   Everything here exists only to satisfy the 44px minimum on touch. It is
   scoped to coarse pointers (and the narrow breakpoint, for browsers that do
   not report pointer type) so the desktop proportions are unchanged: a 41px
   tab is fine under a mouse and the extra height would just look loose.

   The tab keeps its padding and gains a minimum height instead, so short and
   long labels end up the same size. The checkbox and radio stay 18px as
   drawn — they are meant to look like controls, not slabs — and it is their
   label, which activates them, that grows to carry the target. */
@media (max-width: 980px), (pointer: coarse){
  .tab{ min-height:44px; }
  /* The custom select's options come to 43.75px from padding plus line-height
     — close enough to look right and still under the minimum. They are only
     measurable while the list is open, which is why an earlier sweep of the
     page missed them. */
  .sel-opt{ min-height:44px; }
  /* 16px is a threshold, not a taste: iOS Safari zooms the page in when a
     focused field's text is smaller than that, and the visitor is left
     pinching back out after every tap. The fields are 14.5px everywhere else
     and only grow where a soft keyboard can appear. Applies to the custom
     select's button too, which has to keep matching the real inputs. */
  input, select, textarea, .sel-btn{ font-size:16px; }
  .check{ align-items:center; }
  .check label, .opt label{ display:flex; align-items:center; min-height:44px; }
  .opt input[type="radio"], .check input[type="checkbox"]{ margin-top:0; }
}

/* ---------- Mobile menu ---------- */
.mobile-menu a{
  opacity:0; transform:translateY(14px);
  transition:opacity .4s ease, transform .4s cubic-bezier(.22,.68,.32,1);
}
.mobile-menu.open a{ opacity:1; transform:none; }
.mobile-menu.open a:nth-child(2){ transition-delay:.06s; }
.mobile-menu.open a:nth-child(3){ transition-delay:.12s; }
.mobile-menu.open a:nth-child(4){ transition-delay:.18s; }
.mobile-menu.open a:nth-child(5){ transition-delay:.24s; }
.mobile-menu.open a:nth-child(6){ transition-delay:.30s; }
.mobile-menu.open a:nth-child(7){ transition-delay:.36s; }

/* ---------- Counters ----------
   Reserve the final width so the layout does not reflow while the
   number ticks up. */
[data-count]{ font-variant-numeric:tabular-nums; }

/* ============================================================
   Texture layer
   Two quiet layers of depth over the flat ink background:
   a film grain across the whole viewport, and large faceted
   wedges inside each section. Both are deliberately at the
   edge of visibility — everything here sits between 1% and 4%
   opacity so it reads as material, never as content.
   ============================================================ */

/* ---------- Grain ----------
   A fixed, pointer-transparent overlay above everything, which is
   how a film grain actually behaves. Static: animating a viewport-
   sized layer costs a full repaint per frame and buys almost nothing
   at this opacity. */
body::after{
  content:''; position:fixed; inset:0; z-index:400; pointer-events:none;
  opacity:var(--grain-opacity);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:140px 140px;
}

/* ---------- Faceted section fields ----------
   Painted as the section's own background-image rather than a pseudo
   element, so there is no extra stacking context to reason about and
   no risk of a layer landing on top of text. preserveAspectRatio is
   off and the image is sized to the box, so a tall section and a short
   one get different cuts of the same geometry — the rhythm comes free.
   Wedge angles run one way on --ink sections and the other on --ink-2,
   so alternating bands read as facets meeting along an edge.

   Both the band colour and the wedge geometry restart at every section
   boundary, which used to land as a hard horizontal cut across the page.
   --seam-fade is the answer: an opaque --ink stop at each end of the
   section that dissolves into the field within ~14% of the section's
   height. Because it is the topmost background layer it fades the wedges
   AND the --ink-2 band down to the shared body colour, so whatever two
   sections meet, they meet on the same --ink and the join disappears.
   Kept in variables so the fade and the artwork can be recombined per
   case without repeating the data URIs. */
:root{
  --seam-fade:linear-gradient(180deg,
    var(--ink) 0%, rgba(20,19,15,0) 14%,
    rgba(20,19,15,0) 86%, var(--ink) 100%);
  /* For the one section the angled divider hands off to: the divider's
     polygon is already --ink-2, so the colour there is continuous and only
     the wedge artwork needs hiding. Fading the top to --ink-2 rather than
     --ink keeps the handoff invisible while still killing the hard edge
     where the alt field's top triangle would otherwise switch on. */
  --seam-fade-divider:linear-gradient(180deg,
    var(--ink-2) 0%, rgba(27,26,23,0) 14%,
    rgba(20,19,15,0) 86%, var(--ink) 100%);
  /* Heroes only need the bottom half of the treatment: their top is the top
     of the page, but their diagonals run the full height of the box and so
     would otherwise be sliced off square where the hero meets section one. */
  --seam-fade-bottom:linear-gradient(180deg,
    rgba(20,19,15,0) 78%, var(--ink) 100%);
  --tex-section:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cg fill='%23F5C243'%3E%3Cpolygon points='0,0 470,0 180,600 0,600' opacity='.022'/%3E%3Cpolygon points='470,0 880,0 610,600 180,600' opacity='.010'/%3E%3Cpolygon points='880,0 1200,0 1200,600 610,600' opacity='.026'/%3E%3C/g%3E%3Cg fill='%23FFF3C4'%3E%3Cpolygon points='466,0 474,0 184,600 176,600' opacity='.030'/%3E%3Cpolygon points='876,0 884,0 614,600 606,600' opacity='.022'/%3E%3Cpolygon points='0,600 1200,170 1200,600' opacity='.013'/%3E%3C/g%3E%3C/svg%3E");
  --tex-alt:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cg fill='%23F5C243'%3E%3Cpolygon points='0,0 340,0 700,600 300,600' opacity='.020'/%3E%3Cpolygon points='340,0 770,0 1140,600 700,600' opacity='.009'/%3E%3Cpolygon points='770,0 1200,0 1200,600 1140,600' opacity='.024'/%3E%3C/g%3E%3Cg fill='%23FFF3C4'%3E%3Cpolygon points='336,0 344,0 704,600 696,600' opacity='.028'/%3E%3Cpolygon points='0,0 1200,0 0,430' opacity='.011'/%3E%3C/g%3E%3C/svg%3E");
  /* Hero diagonals, kept in a variable for the same reason as the rest:
     they are near-white over black and need a bronze counterpart. */
  --tex-hero:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 400' preserveAspectRatio='none'%3E%3Cpolygon points='640,0 1070,0 1132,400 894,400' fill='%23F5C243' opacity='.020'/%3E%3Cg fill='%23FFF3C4'%3E%3Cpolygon points='635,0 645,0 903,400 893,400' opacity='.028'/%3E%3Cpolygon points='1065,0 1075,0 1137,400 1127,400' opacity='.020'/%3E%3C/g%3E%3C/svg%3E");
}
/* ============================================================
   Light theme

   Not an inversion. The dark page is warm black with cards lifting
   *brighter* than the field; the light page is warm bone with cards
   lifting brighter than the field too — same relationship, same
   material, different lighting. Everything that makes the dark page
   feel built — the grain, the faceted wedges, the sweeps, the pointer
   glow, the seam fades — is restated here rather than dropped, because
   a light theme that is only "the dark one with the lights on" reads
   as an afterthought.

   Two rules govern every value below.

   1. Gold splits by job. On a surface that is meant to look like metal
      (the primary button, the checkbox, the radio) gold stays gold. As
      lettering or as a mark on paper it goes deep amber, because the
      bright golds are 1.5:1 against bone and would be unreadable.
   2. Effects darken instead of lightening. Over near-black, a 12% gold
      wash lifts the surface and reads as light. Over near-white it does
      nothing at all (a 1.02 shift). So every sweep, tint and glow below
      is a *shadow* in gold, not a highlight.

   Deliberate black in the light theme, for rhythm rather than for
   contrast alone: the final CTA band becomes a full dark slab on every
   page that has one, and the active tab becomes a near-black pill. Both
   are noted at their rules. Headlines are near-black (#17150F), not a
   soft grey.

   Every text/background pair here was measured; the audit is in the
   commit message.
   ============================================================ */
:root[data-theme="light"]{
  /* Backgrounds: warm off-white, never stark. Cards are the brightest
     of the three so they lift, exactly as --ink-3 does in the dark. */
  --ink: #F7F4EE;
  --ink-2: #EEE9DE;
  --ink-3: #FFFDF7;

  /* Text, dark-on-light. Warm near-black rather than pure black, to
     stay in the same family as the gold. 16.6:1 on the page. */
  --paper: #17150F;
  --paper-dim: #3A352A;
  --text-dim: #5A5346;
  --text-dim-2: #6E6656;

  /* Gold surfaces stay gold — the button must still read as metal.
     --gold-1 is only ever a gradient stop, so it keeps its highlight. */
  --gold-1: #FFF3C4;
  --gold-2: #F0C33F;
  --gold-3: #E0AE2E;
  --gold-4: #AE7420;
  --gold-5: #8A5A12;
  --gold-6: #4A2F09;

  /* Gold as lettering, and as an edge that has to be seen: both have to
     go deep amber on a pale page. 7.4:1 and 6.2:1 respectively. */
  --gold-ink: #6B4907;
  --gold-ink-2: #7A5409;
  --gold-edge: #AE7420;
  /* Drawn marks — dots, nodes, bullets. 4.3:1, clear of the 3:1 bar
     that non-text content has to meet. */
  --gold-mark: #A06716;

  --line: rgba(45,38,22,0.30);
  --line-strong: rgba(45,38,22,0.42);
  /* 3.1:1 on the field fill, 3.5:1 on the page. */
  --field-edge: #8A8272;
  /* These two carry a dependency on main.js worth stating, because the light
     theme is the only place it bites. Over the page both are fine (nav text
     6.9:1). Over the dark CTA slab the at-rest 0.72 header would put nav text
     at 3.74:1 — under AA — while the scrolled 0.92 one holds 5.89:1.

     That is safe today only because main.js adds .scrolled at y > 12, so the
     0.72 state exists only in the top 12px of the page, where the thing
     behind the header is always the page background and never the slab. If
     that threshold grows, or a dark section is ever placed high enough to
     sit under an unscrolled header, raise this alpha to 0.92 as well. */
  --header-bg: rgba(247,244,238,0.72);
  --header-bg-scrolled: rgba(247,244,238,0.92);
  /* Warm brown shadows; black would read as grime on a warm page. */
  --shadow-1: rgba(60,48,20,0.18);
  --shadow-2: rgba(60,48,20,0.30);

  /* ---- Effect layer, restated as darkening ---- */
  --tint-faint: rgba(138,90,18,0.07);
  --tint-soft: rgba(138,90,18,0.13);
  --tint-mid: rgba(138,90,18,0.15);

  --edge-soft: rgba(174,116,32,0.45);
  --edge-mid: rgba(174,116,32,0.55);
  --edge-strong: rgba(174,116,32,0.62);

  /* The sweep is the one effect that has to change sign outright: a
     cream band over a cream card is invisible, so on paper the light
     passes as a faint bronze shadow instead. Same geometry, same
     timing — it reads as a facet catching the light either way. */
  /* Alphas here are not guesses and not copies of the dark ones: each was
     solved so the effect moves the card surface by the same amount its dark
     counterpart does. Measured as contrast against the card the effect lands
     on — sweep 1.29 vs dark 1.29, corner glow 1.31 vs 1.31, pointer glow
     1.35 vs 1.34. Carrying the dark alphas across unchanged left all three
     visibly weaker than dark (1.15 / 1.19 / 1.23), which is exactly the
     "looks like an afterthought" failure. */
  --sweep: rgba(138,90,18,0.18);
  --cursor-glow: rgba(160,103,22,0.23);
  --glow-soft: rgba(160,103,22,0.21);
  /* A bloom cannot brighten paper, so the dot's halo becomes a tight warm
     shadow — the dot still "lights up", it just does it by gaining weight
     rather than by glowing. This is the one effect that cannot reach parity:
     a halo on bone can only darken toward its own colour, so matching dark's
     4.71 would need a near-black smudge round an 8px dot. Deep brown at 0.62
     gets it to ~4.4 and still reads as a bloom rather than a blot. */
  --glow-dot: rgba(46,29,5,0.62);
  --glow-node: rgba(110,72,12,0.42);
  --hero-bloom: rgba(160,103,22,0.09);

  /* Gradients. Each dark one ends on --gold-1/--gold-2, which are
     1.1:1 and 1.5:1 on bone — they would simply stop existing. Each
     light one runs bronze-to-amber instead, keeping the same
     dark-bright-dark shape that reads as a bevel. */
  --grad-head: linear-gradient(100deg, var(--gold-6), var(--gold-4) 50%, var(--gold-5));
  --grad-progress: linear-gradient(90deg, var(--gold-6), var(--gold-mark) 60%, var(--gold-4));
  --grad-underline: linear-gradient(90deg, var(--gold-5), var(--gold-4));
  --grad-shard: linear-gradient(140deg, var(--gold-3), var(--gold-4) 55%, var(--gold-6));
  --grad-step-line: linear-gradient(180deg, var(--gold-5), var(--gold-mark) 45%, var(--gold-5) 85%, transparent);

  /* Deliberate black, 1 of 2. A gold pill on bone is 1.5:1 against the
     page — the selected tab would be the *least* visible control in the
     row. Near-black with gold lettering is 16.6:1 against the page and
     12.9:1 internally, and it gives the tab row the same punch the gold
     pill has on the dark page. */
  --tab-on-bg: #17150F;
  --tab-on-fg: var(--gold-2);
  --tab-on-edge: #17150F;

  --node-lit: var(--gold-mark);
  --glyph-ink: #8A5A12;
  --glyph-opacity: 0.78;

  /* Grey noise darkens bone faster than it lightens near-black, but the
     page is also brighter, so the grain needs more of it to read as
     material rather than as dirt. Tuned by eye at 1400px. */
  --grain-opacity: 0.050;

  color-scheme: light;
}

/* The seam fades hardcode the page colour at zero alpha, so they need
   restating per theme or the fade would blend toward the dark page. */
:root[data-theme="light"]{
  --seam-fade:linear-gradient(180deg,
    var(--ink) 0%, rgba(247,244,238,0) 14%,
    rgba(247,244,238,0) 86%, var(--ink) 100%);
  --seam-fade-divider:linear-gradient(180deg,
    var(--ink-2) 0%, rgba(238,233,222,0) 14%,
    rgba(247,244,238,0) 86%, var(--ink) 100%);
  --seam-fade-bottom:linear-gradient(180deg,
    rgba(247,244,238,0) 78%, var(--ink) 100%);
  /* The faceted wash is gold-on-black in the dark theme, which vanishes
     on a pale page. Same geometry, darkened and lifted in opacity so the
     facets still read — measured to land at roughly the same visual
     weight as the dark theme's wedges, not fainter. */
  --tex-section:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cg fill='%238A5A12'%3E%3Cpolygon points='0,0 470,0 180,600 0,600' opacity='.055'/%3E%3Cpolygon points='470,0 880,0 610,600 180,600' opacity='.026'/%3E%3Cpolygon points='880,0 1200,0 1200,600 610,600' opacity='.064'/%3E%3C/g%3E%3Cg fill='%234A2F09'%3E%3Cpolygon points='466,0 474,0 184,600 176,600' opacity='.070'/%3E%3Cpolygon points='876,0 884,0 614,600 606,600' opacity='.052'/%3E%3Cpolygon points='0,600 1200,170 1200,600' opacity='.030'/%3E%3C/g%3E%3C/svg%3E");
  --tex-alt:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cg fill='%238A5A12'%3E%3Cpolygon points='0,0 340,0 700,600 300,600' opacity='.048'/%3E%3Cpolygon points='340,0 770,0 1140,600 700,600' opacity='.024'/%3E%3Cpolygon points='770,0 1200,0 1200,600 1140,600' opacity='.058'/%3E%3C/g%3E%3Cg fill='%234A2F09'%3E%3Cpolygon points='336,0 344,0 704,600 696,600' opacity='.066'/%3E%3Cpolygon points='0,0 1200,0 0,430' opacity='.026'/%3E%3C/g%3E%3C/svg%3E");
  /* The hero's own field, same treatment: the dark theme's near-white
     diagonals become bronze ones. Without this the hero — the first
     thing anyone sees — is the one flat area on the light page. */
  --tex-hero:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 400' preserveAspectRatio='none'%3E%3Cpolygon points='640,0 1070,0 1132,400 894,400' fill='%238A5A12' opacity='.048'/%3E%3Cg fill='%234A2F09'%3E%3Cpolygon points='635,0 645,0 903,400 893,400' opacity='.070'/%3E%3Cpolygon points='1065,0 1075,0 1137,400 1127,400' opacity='.050'/%3E%3C/g%3E%3C/svg%3E");
}

/* Deliberate black, 2 of 2 — the closing CTA band, on all six pages
   that carry one.

   On the dark page this band is a subtle lift (--ink-2 to --ink-3) and
   the gold button supplies the punch. On a light page that same lift is
   invisible, and the band — the last thing on the page, and the actual
   ask — would be its weakest moment. So it inverts to a full dark slab:
   the page ends on a deep, faceted block with gold on it, which is both
   the rhythm break a long light page needs and the strongest possible
   frame for the button.

   The slab re-declares the dark theme's own text and line values rather
   than inheriting the light ones, so everything inside it is back on a
   dark footing: --paper 16.7:1, --text-dim 6.9:1, gold 12.9:1. */
:root[data-theme="light"] .cta-band{
  --paper: #F4F5F3;
  --paper-dim: #E9E4D9;
  --text-dim: #A79E8C;
  --gold-ink: #FFD54B;
  --gold-ink-2: #F5C243;
  --line: rgba(233,228,217,0.14);
  background:linear-gradient(135deg, #17150F, #241F16);
  /* Stated separately from the shorthand, and kept after it, so the slab has
     an opaque dark colour even if the gradient layer is not painted. Without
     it the band's background-color is transparent, and the light page would
     show through under white text. */
  background-color:#17150F;
  border-color:rgba(233,228,217,0.14);
  color:var(--paper);
  /* The wedges are the same artwork the dark sections use, so the slab
     is faceted like the rest of the site rather than a plain rectangle. */
  background-image:
    linear-gradient(135deg, #17150F, #241F16),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cg fill='%23F5C243'%3E%3Cpolygon points='0,0 470,0 180,600 0,600' opacity='.030'/%3E%3Cpolygon points='880,0 1200,0 1200,600 610,600' opacity='.034'/%3E%3C/g%3E%3Cg fill='%23FFF3C4'%3E%3Cpolygon points='466,0 474,0 184,600 176,600' opacity='.038'/%3E%3C/g%3E%3C/svg%3E");
  background-size:100% 100%;
  background-repeat:no-repeat;
  box-shadow:0 26px 60px -34px rgba(40,30,8,0.55);
}

/* Elevation, light theme only.

   On the dark page a card lifts by being *brighter* than the field, and a
   shadow under it would be invisible against near-black — so there is none.
   On a light page that lift is the weaker of the two signals available and
   the shadow is the stronger one, which is why paper-coloured interfaces are
   built out of shadows. Without it these cards read as outlines rather than
   objects. Warm brown rather than black: neutral shadow on a warm page looks
   like dirt.

   Two layers on purpose — a tight one for the contact edge and a wide, soft
   one for the cast — because a single mid shadow reads as a blur. */
:root[data-theme="light"] .svc-card,
:root[data-theme="light"] .teaser-card,
:root[data-theme="light"] .work-card,
:root[data-theme="light"] .quote-doc,
:root[data-theme="light"] .cat-visual{
  box-shadow:0 1px 2px -1px rgba(60,48,20,0.10),
             0 14px 30px -20px rgba(60,48,20,0.30);
}
/* The cards that already translate on hover get a deeper cast to go with it,
   so the rise reads as the card leaving the page rather than just sliding. */
:root[data-theme="light"] .svc-card:hover,
:root[data-theme="light"] .teaser-card:hover,
:root[data-theme="light"] .work-card:hover{
  box-shadow:0 2px 4px -2px rgba(60,48,20,0.12),
             0 24px 44px -24px rgba(60,48,20,0.38);
}
@media (prefers-reduced-motion: reduce){
  /* The cards do not move under reduced motion, so the deeper cast would be
     a shadow with nothing casting it. */
  :root[data-theme="light"] .svc-card:hover,
  :root[data-theme="light"] .teaser-card:hover,
  :root[data-theme="light"] .work-card:hover{
    box-shadow:0 1px 2px -1px rgba(60,48,20,0.10),
               0 14px 30px -20px rgba(60,48,20,0.30);
  }
}


/* ---------- Theme toggle ----------
   Hidden without JS: the button cannot do anything there, and a control
   that does nothing is the thing we removed twice already this session. */
.theme-btn{ display:none; }
:where(.js) .theme-btn{
  /* Grid rather than flex so both icons occupy the SAME cell: they cross-fade
     in place instead of one replacing the other, and the button never changes
     size mid-swap. */
  display:grid; place-items:center;
  /* 44px, not 40: this is a real touch target on phones, and it sits next to
     a .menu-btn that is already held at 44. */
  width:44px; height:44px; padding:0; flex-shrink:0;
  background:transparent; border:1.5px solid var(--line); border-radius:999px;
  color:var(--text-dim); cursor:pointer; overflow:hidden;
  transition:color .25s ease, border-color .25s ease, background .25s ease;
}
.theme-btn:hover{ border-color:var(--gold-4); color:var(--gold-ink); background:var(--tint-soft); }
.theme-btn:active{ transform:scale(.93); }

/* The icon shows the theme you would switch TO, and the label says so
   outright so the direction is never guesswork for a screen reader.

   The swap is a rotation through the button rather than a display flip: the
   outgoing icon turns away and shrinks out, the incoming one turns in from
   the other side. The overshoot in the timing function is what makes it feel
   mechanical — like a dial landing — rather than like a fade. */
.theme-btn svg{
  grid-area:1 / 1;
  width:17px; height:17px; display:block;
  transition:opacity .3s ease, transform .45s cubic-bezier(.34,1.32,.5,1);
}
.theme-btn .icon-sun{ opacity:1; transform:none; }
.theme-btn .icon-moon{ opacity:0; transform:rotate(-100deg) scale(.4); }
:root[data-theme="light"] .theme-btn .icon-sun{ opacity:0; transform:rotate(100deg) scale(.4); }
:root[data-theme="light"] .theme-btn .icon-moon{ opacity:1; transform:none; }

.nav-end{ display:flex; align-items:center; gap:14px; }

/* Cross-fade the repaint, but only while a toggle is in flight — a
   permanent global transition would fight every hover rule on the site. */
:root.theme-switching, :root.theme-switching *,
:root.theme-switching *::before, :root.theme-switching *::after{
  transition:background-color .3s ease, background-image .3s ease,
             color .3s ease, border-color .3s ease, fill .3s ease, stroke .3s ease !important;
}
@media (prefers-reduced-motion: reduce){
  :root.theme-switching, :root.theme-switching *,
  :root.theme-switching *::before, :root.theme-switching *::after{ transition:none !important; }
  /* The icon still changes — it has to, it is the control's only state — it
     just arrives already turned, with no spin and no squash. */
  .theme-btn svg{ transition:none; }
  .theme-btn .icon-moon{ transform:none; }
  :root[data-theme="light"] .theme-btn .icon-sun{ transform:none; }
  .theme-btn:active{ transform:none; }
}

section{
  background-repeat:no-repeat;
  background-position:center;
  background-size:100% 100%;
  background-image:var(--seam-fade), var(--tex-section);
}
.alt-bg{
  background-color:var(--ink-2);
  background-image:var(--seam-fade), var(--tex-alt);
}
/* The angled divider already hands the hero off to the band below it, so
   that join fades to --ink-2 at the top instead of --ink — see the note on
   --seam-fade-divider. */
.facet-divider + .alt-bg{
  background-image:var(--seam-fade-divider), var(--tex-alt);
}
/* Heroes are the exception: both of them sit inside .wrap, so their box
   stops short of the viewport. A wedge flush to that box edge would read
   as a panel seam rather than a facet, so the hero field is built only
   from diagonals that terminate inside the box, plus a soft gold bloom.

   That field must not be painted on the .wrap box itself. background-size
   is resolved against the padding box, so a bloom that is meant to fall
   off softly instead got sliced flat at x = the wrap edge, printing a
   dark rectangle down the left of the hero where the lit area stopped.
   It is painted on a full-bleed pseudo element instead: the content stays
   inside .wrap, the light spans the viewport, and there is no box to see.
   z-index:-1 keeps it behind the hero's own content, and neither hero
   creates a stacking context, so it still sits above the body colour. */
.page-hero, section.hero{ position:relative; background-image:none; }
.page-hero::before, section.hero::before{
  content:''; position:absolute; z-index:-1; pointer-events:none;
  top:0; bottom:0; left:calc(50% - 50vw); width:100vw;
  background-repeat:no-repeat;
  background-position:center;
  background-size:100% 100%;
  background-image:
    var(--seam-fade-bottom),
    radial-gradient(760px 440px at 28% 32%, var(--hero-bloom), transparent 72%),
    var(--tex-hero);
}

/* ============================================================
   Trust blocks
   Plain, concrete statements of what happens next. Nothing here
   is decorative — each item restates a commitment already made
   elsewhere on the site.
   ============================================================ */
.assure{
  margin-top:36px; padding-top:28px; border-top:1px solid var(--line);
}
.assure ol{ list-style:none; counter-reset:assure; display:grid; gap:16px; margin-top:14px; }
.assure li{
  position:relative; padding-left:38px; font-size:14.5px; color:var(--text-dim);
  max-width:360px;
}
.assure li strong{ color:var(--paper-dim); font-weight:600; }
.assure li::before{
  counter-increment:assure; content:counter(assure);
  position:absolute; left:0; top:-1px;
  width:24px; height:24px; display:flex; align-items:center; justify-content:center;
  font-size:11.5px; font-weight:700; color:var(--gold-ink);
  background:var(--tint-soft);
  clip-path:polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}
.form-note{
  font-size:12.5px; color:var(--text-dim-2); max-width:420px; margin-top:-4px;
}

/* Deliverable line closing each process stage. */
.step-out{
  margin-top:18px; padding:12px 16px;
  border-left:2px solid var(--gold-4);
  background:var(--tint-faint);
  font-size:14px; color:var(--paper-dim);
}
.step-out b{ color:var(--gold-ink); font-weight:600; }

/* What the quote document contains — pricing page. */
.quote-doc{
  margin-top:44px; border:1px solid var(--line); background:var(--ink-3);
  padding:34px 34px 30px;
  clip-path:polygon(0 0, calc(100% - 22px) 0, 100% 22px, 100% 100%, 22px 100%, 0 calc(100% - 22px));
}
/* On an alt-bg section the panel's own --ink-3 sits only a few values off
   the ground and the edge disappears. Drop it to --ink so it reads as a
   recessed panel instead, and firm up the border. Scoped to .alt-bg, so the
   pricing page's panel (on a plain section) is untouched. */
.alt-bg .quote-doc{ background:var(--ink); border-color:var(--line-strong); }
.quote-doc h3{ font-size:19px; margin-bottom:8px; }
.quote-doc > p{ font-size:14.5px; color:var(--text-dim); max-width:620px; }
.quote-doc ol{
  list-style:none; counter-reset:qd; display:grid; grid-template-columns:repeat(2,1fr);
  gap:16px 30px; margin-top:24px;
}
.quote-doc li{ position:relative; padding-left:30px; font-size:14.5px; color:var(--text-dim); }
.quote-doc li strong{ display:block; color:var(--paper); font-weight:600; font-size:15px; margin-bottom:3px; }
.quote-doc li::before{
  counter-increment:qd; content:'◆';
  position:absolute; left:4px; top:2px; font-size:11px; color:var(--gold-ink-2);
}
@media (max-width: 760px){
  .quote-doc{ padding:26px 22px; }
  .quote-doc ol{ grid-template-columns:1fr; }
}

/* ============================================================
   Send confirmation
   The one moment of delight on the site: the submit button resolves
   into a drawn checkmark and throws a short burst of gold facet
   shards. It has a job — the form posts in the background, so this
   is the only signal the visitor gets that the message left.
   ============================================================ */
.sent-panel{
  border:1px solid var(--edge-mid); background:var(--ink-2);
  padding:34px 30px; text-align:center;
  clip-path:polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
}
.sent-panel h2{ font-size:21px; margin-bottom:10px; }
.sent-panel p{ font-size:14.5px; color:var(--text-dim); max-width:420px; margin:0 auto; }
.sent-mark{
  position:relative; width:62px; height:62px; margin:0 auto 20px;
  display:flex; align-items:center; justify-content:center;
}
.sent-mark svg{ width:62px; height:62px; overflow:visible; }
.sent-mark .ring{
  fill:none; stroke:var(--gold-ink-2); stroke-width:2; opacity:.55;
  stroke-dasharray:220; stroke-dashoffset:220;
  animation:mark-ring .5s cubic-bezier(.22,.68,.32,1) forwards;
}
.sent-mark .tick{
  fill:none; stroke:var(--gold-ink); stroke-width:5; stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:40; stroke-dashoffset:40;
  animation:mark-tick .35s cubic-bezier(.3,.7,.4,1) .28s forwards;
}
@keyframes mark-ring{ to{ stroke-dashoffset:0; } }
@keyframes mark-tick{ to{ stroke-dashoffset:0; } }

/* Shards are placed by JS as --a (angle) / --d (travel). Each is a
   small gold facet, not a circle, so the burst stays on-motif. */
.shard{
  position:absolute; left:50%; top:50%; width:9px; height:9px; margin:-4.5px 0 0 -4.5px;
  background:var(--grad-shard);
  clip-path:polygon(50% 0, 100% 38%, 78% 100%, 22% 100%, 0 38%);
  pointer-events:none; opacity:0;
  animation:shard-out .72s cubic-bezier(.16,.72,.3,1) var(--sd, 0ms) forwards;
}
@keyframes shard-out{
  0%{ opacity:0; transform:rotate(var(--a)) translateX(6px) rotate(0deg) scale(.4); }
  18%{ opacity:1; }
  100%{ opacity:0; transform:rotate(var(--a)) translateX(var(--d)) rotate(220deg) scale(.85); }
}

/* form carries display:grid, which outranks the UA rule for [hidden] —
   without this the form stays on screen behind the confirmation. */
[hidden]{ display:none !important; }

/* Button while the request is in flight. */
.btn[aria-disabled="true"]{ opacity:.65; cursor:default; }
.btn[aria-disabled="true"]:hover{ transform:none; }

/* ---------- Language switcher ----------
   Built by assets/i18n.js, so like the theme toggle it only exists with JS.
   Same 44px pill and hover treatment as .theme-btn next to it; the menu is
   the custom select's list, anchored to the right edge of the header. */
.lang{ position:relative; }
.lang-btn{
  height:44px; min-width:44px; padding:0 14px; border-radius:999px;
  background:transparent; border:1.5px solid var(--line); color:var(--text-dim);
  font-family:inherit; font-size:12.5px; font-weight:600; letter-spacing:.08em;
  cursor:pointer;
  transition:color .25s ease, border-color .25s ease, background .25s ease;
}
.lang-btn:hover, .lang[data-open="true"] .lang-btn{
  border-color:var(--gold-4); color:var(--gold-ink); background:var(--tint-soft);
}
.lang-menu{
  position:absolute; z-index:120; top:calc(100% + 8px); right:0; min-width:172px;
  padding:5px; background:var(--ink-3); border:1px solid var(--gold-5); border-radius:8px;
  box-shadow:0 18px 40px var(--shadow-1);
  opacity:0; transform:translateY(-6px); pointer-events:none; visibility:hidden;
  transition:opacity .2s ease, transform .3s cubic-bezier(.22,.68,.32,1), visibility 0s linear .3s;
}
.lang[data-open="true"] .lang-menu{
  opacity:1; transform:none; pointer-events:auto; visibility:visible;
  transition:opacity .2s ease, transform .3s cubic-bezier(.22,.68,.32,1), visibility 0s;
}
.lang-opt{
  display:flex; align-items:center; gap:12px; width:100%; min-height:44px;
  padding:10px 12px; border:0; border-radius:6px; background:transparent;
  font-family:inherit; font-size:14.5px; color:var(--paper-dim); text-align:left;
  cursor:pointer; transition:background-color .15s ease, color .15s ease;
}
.lang-opt:hover, .lang-opt:focus-visible{ background:var(--tint-mid); color:var(--paper); }
.lang-opt[aria-current="true"]{ color:var(--gold-ink); font-weight:600; }
.lang-code{ min-width:22px; font-size:11.5px; font-weight:600; letter-spacing:.08em; color:var(--text-dim-2); }
.lang-opt[aria-current="true"] .lang-code{ color:var(--gold-ink); }

/* Making room for it. The switcher takes ~60px of a header that was already
   close to full between the 980px breakpoint and the max width, so in that
   band only, the gap between nav links eases down instead of the links
   wrapping onto two lines. At full width nothing changes.
   Spanish labels run ~150px longer than English ("Por qué nosotros",
   "Empieza un proyecto"), which leaves no room at any desktop width at the
   usual 36px, so Spanish gets a tighter gap and hands over to the menu
   button a little earlier. */
nav.links a, .nav-cta .btn{ white-space:nowrap; }
@media (min-width: 981px) and (max-width: 1180px){
  nav.links{ gap:clamp(16px, 10vw - 82px, 36px); }
}
:root:lang(es) nav.links{ gap:20px; }
@media (max-width: 1180px){
  :root:lang(es) nav.links, :root:lang(es) .nav-cta{ display:none; }
  :root:lang(es) .menu-btn{
    display:inline-flex; align-items:center; justify-content:center;
    min-width:44px; min-height:44px;
  }
}

/* A non-English page is held hidden until its text is swapped in (the class
   is set by the inline script in the head, cleared by i18n.js). */
:root.i18n-pending body{ visibility:hidden; }
/* Switching on a page already in view: the copy dips out for the swap and
   comes back, instead of every line jumping at once. */
main, footer, nav.links, .nav-cta{ transition:opacity .18s ease; }
.lang-switching main, .lang-switching footer,
.lang-switching nav.links, .lang-switching .nav-cta{ opacity:0; }

/* ---------- Motion refinements ----------
   The same movements as above, retimed. Anything that travels gets a longer,
   decelerating curve so it never starts or stops abruptly; fades run a touch
   ahead of the movement they accompany. Scoped to no-preference, so reduced
   motion keeps exactly the rules it had. */
@media (prefers-reduced-motion: no-preference){
  :root{
    --ease-glide:cubic-bezier(.22,.68,.32,1);
    --ease-fade:cubic-bezier(.33,1,.68,1);
    --ease-sheet:cubic-bezier(.7,0,.2,1);
  }
  .btn{ transition:transform .45s var(--ease-glide); }
  .btn-primary::before{ transition:transform .9s var(--ease-glide); }
  header{ transition:background .45s ease, border-color .45s ease, box-shadow .45s ease; }
  nav.links a{ transition:color .3s ease; }
  nav.links a::after{ transition:transform .45s var(--ease-glide); }
  .mobile-menu{ transition:transform .6s var(--ease-sheet), visibility .6s; }
  .mobile-menu a{ transition:opacity .5s var(--ease-fade), transform .65s var(--ease-glide); }

  :where(.js) .reveal{
    transition:opacity .9s var(--ease-fade), transform 1.05s var(--ease-glide);
    transition-delay:var(--reveal-delay, 0ms);
  }

  .svc-card, .teaser-card{ transition:border-color .3s ease, transform .55s var(--ease-glide), box-shadow .55s ease; }
  .work-card{ transition:transform .55s var(--ease-glide), border-color .3s ease, box-shadow .55s ease; }
  .svc-card::before, .teaser-card::before, .work-card::after, .quote-doc::before{ transition:transform .95s var(--ease-glide); }
  .svc-card::after, .teaser-card::after, .quote-doc::after{ transition:opacity .5s ease; }
  .step::before{ transition:color .35s ease, transform .45s var(--ease-glide); }
  .why-item{ transition:background .45s ease; }
  .why-item h3 .dot{ transition:transform .6s var(--ease-glide), box-shadow .45s ease; }

  .tab{ transition:border-color .3s ease, color .3s ease, background .3s ease, transform .3s var(--ease-glide); }
  :where(.js) .tab-panel.active > *{ animation-duration:.7s; }

  .faq-a{ transition:max-height .55s var(--ease-glide); }
  :where(.js) .faq-a p{ transition:transform .6s var(--ease-glide); }
  .faq-q .plus{ transition:transform .45s var(--ease-glide); }

  .sel-caret{ transition:transform .4s var(--ease-glide); }
  .sel-list{ transition:opacity .22s ease, transform .32s var(--ease-glide), visibility 0s linear .32s; }
  .sel[data-open="true"] .sel-list{ transition:opacity .22s ease, transform .32s var(--ease-glide), visibility 0s; }

  /* Interior page heroes arrive with a soft rise instead of appearing
     outright. The container moves, not the h1/p inside it, because those
     carry the scroll parallax as inline transforms. Held until a translated
     page is ready, so the rise is never spent while the page is hidden. */
  :root:not(.i18n-pending) .page-hero{ animation:hero-in .95s var(--ease-glide) both; }
}
@keyframes hero-in{
  from{ opacity:0; transform:translateY(16px); }
  to{ opacity:1; transform:none; }
}

/* ---------- Reduced motion ----------
   Anything above that moves is stripped back to a plain state. */
@media (prefers-reduced-motion: reduce){
  .lang-menu, .lang[data-open="true"] .lang-menu{ transition:none; transform:none; }
  main, footer, nav.links, .nav-cta{ transition:none; }
  :where(.js) .reveal{ opacity:1; transform:none; transition:none; transition-delay:0ms !important; }
  .scroll-progress{ display:none; }
  .svc-card::before, .teaser-card::before, .work-card::after,
  .svc-card::after, .teaser-card::after,
  .quote-doc::before, .quote-doc::after{ display:none; }
  .svc-card:hover, .teaser-card:hover, .work-card:hover{ transform:none; }
  .work-card:hover .work-glyph{ transform:none; }
  .work-card .work-glyph{ transition:none; }
  .tab-panel.active > *{ animation:none; }
  .mobile-menu a{ opacity:1; transform:none; transition:none; }
  .faq-a p{ transform:none; transition:none; }
  .why-item:hover h3 .dot{ transform:rotate(45deg); box-shadow:none; }
  nav.links a::after{ transition:none; }
  /* Confirmation: the checkmark and panel still appear, they just
     arrive already drawn. The shard burst is dropped entirely. */
  .sent-mark .ring, .sent-mark .tick{ animation:none; stroke-dashoffset:0; }
  .shard{ display:none; }
  /* The meeting fields appear at once instead of sliding in. */
  .meet-fields{ animation:none; }
  /* Custom select: it still opens and closes, it just does it at once. */
  .sel-btn::before{ display:none; }
  .sel-caret{ transition:none; }
  .sel-list, .sel[data-open="true"] .sel-list{ transition:none; transform:none; }
  .sel-opt{ transition:none; }
  /* Texture is static anyway; stated here so it stays that way. */
  body::after{ animation:none; }
}

/* Cascade the cards inside a freshly-activated tab panel. */
.tab-panel.active > *:nth-child(2){ --card-delay:60ms; }
.tab-panel.active > *:nth-child(3){ --card-delay:120ms; }
.tab-panel.active > *:nth-child(4){ --card-delay:180ms; }
.tab-panel.active > *:nth-child(5){ --card-delay:240ms; }
.tab-panel.active > *:nth-child(6){ --card-delay:300ms; }
.tab-panel.active > *:nth-child(7){ --card-delay:360ms; }

