/* ══════════════════════════════════════════════════════════════
   SORT — v2 · entropy engine

   Concept: disorder resolving into order. Two accents encode it —
   a warm one for chaos, a cool one for settled — and the hero field
   literally moves between them.

   TYPE IS TUNED FOR THE ACTUAL AUDIENCE: business owners and firm
   partners, largely 45+, often reading on a laptop in daylight. That
   drives three decisions throughout —

     1. Fraunces, not a hairline display serif. Its optical-size axis
        is deliberately pinned BELOW the rendered size, so headings
        keep the sturdy low-contrast strokes of a text cut instead of
        thinning out as they grow.
     2. Body copy starts at 18px and no text anywhere is below 13px.
        Everything is in rem, so a reader who has set their browser
        to 20px gets a layout that grows with them.
     3. Every text colour clears 4.5:1 on paper; small text clears 5:1.
        The warm accent exists in two values — a bright one for marks
        and borders, a darker one that is safe to actually read.

   Interaction conventions follow Apple's: translucent materials with
   soft scroll edges, feedback on press rather than release, and
   reduced-motion / reduced-transparency / high-contrast as
   first-class states.

   Anything a finger can grab is animated in script.js with springs,
   NOT with the transitions below — a CSS transition can't be caught
   and reversed mid-flight.
   ══════════════════════════════════════════════════════════════ */

:root{
  /* surface */
  --paper:      #F7F5F0;
  --paper-2:    #EFEBE3;
  --paper-3:    #FFFFFF;

  /* ink — all three clear 4.5:1 on paper; --ink-3 clears 5:1 */
  --ink:        #14161A;
  --ink-2:      #3B424A;
  --ink-3:      #5C636B;

  /* order — river water, where things have settled */
  --order:      #2A6379;            /* 6.0:1 — safe for text */
  --order-2:    #1C4655;            /* 9.4:1 */
  --order-soft: #DEEBEF;

  /* chaos — the unresolved half.
     --chaos is for marks, borders and graphics only (3.9:1).
     --chaos-2 is the one that may carry words (7.1:1). */
  --chaos:      #B84A24;
  --chaos-2:    #8F3616;
  --chaos-soft: #F8E7DF;

  --line:       #E0DBD1;
  --line-2:     #CBC4B6;

  /* materials */
  --material-bg:   rgba(247, 245, 240, .74);
  --material-edge: rgba(255, 255, 255, .55);
  --blur:          20px;

  /* type */
  --display: "Fraunces", "Iowan Old Style", Charter, Georgia, serif;
  --sans:    "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Nothing on this page renders smaller than --fs-min (13px at
     default settings). Labels that want to be tiny get bolder and
     darker instead of smaller. */
  --fs-min:  .8125rem;
  --fs-sm:   .9375rem;
  --fs-md:   1.0625rem;

  /* rhythm — em/rem throughout, so a bumped browser text size scales
     the layout with the type instead of breaking it */
  --wrap:    1180px;
  --gutter:  clamp(1.25rem, 5vw, 3rem);
  --section: clamp(4.25rem, 8.5vw, 7rem);
  --nav-h:   4.5rem;

  /* Mirrored pair, so a reversible transition retraces its own path.
     The inverse of cubic-bezier(x1,y1,x2,y2) is (1-x2, 1-y2, 1-x1, 1-y1). */
  --ease-out: cubic-bezier(.22,.61,.36,1);
  --ease-in:  cubic-bezier(.64,0,.78,.39);
}

/* ── reset ─────────────────────────────────────────────────── */
*,*::before,*::after{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; scroll-behavior:smooth; }
@media (prefers-reduced-motion:reduce){ html{ scroll-behavior:auto; } }

body{
  margin:0;
  background:var(--paper);
  color:var(--ink);
  font-family:var(--sans);
  font-optical-sizing:auto;
  font-size:clamp(1.125rem, 1.06rem + .3vw, 1.25rem);   /* 18 → 20px */
  line-height:1.68;
  letter-spacing:0;                 /* body sits at zero; only the extremes move */
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  /* clip, not hidden — `hidden` would make body a scroll container and
     break the sticky nav */
  overflow-x:clip;
}

/* Paper grain. Kept light — texture must not eat into text contrast. */
body::after{
  content:"";
  position:fixed; inset:0;
  z-index:900;
  pointer-events:none;
  opacity:.26;
  mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.3'/%3E%3C/svg%3E");
}

h1,h2,h3{ margin:0; text-wrap:balance; }
p{ margin:0 0 1em; text-wrap:pretty; }
ul,ol{ margin:0; padding:0; list-style:none; }
img,svg,canvas{ max-width:100%; }
a{ color:inherit; }

/* Kill the legacy ~300ms tap delay on every interactive target */
a,button{ touch-action:manipulation; }
button{ font:inherit; color:inherit; background:none; border:0; cursor:pointer; }

::selection{ background:var(--chaos-2); color:var(--paper); }

:focus-visible{
  outline:3px solid var(--order);   /* thick enough to find without hunting */
  outline-offset:3px;
  border-radius:.25rem;
}

.skip{
  position:absolute; left:-9999px; top:0;
  background:var(--ink); color:var(--paper);
  padding:.85rem 1.35rem; z-index:1000; font-size:var(--fs-md);
}
.skip:focus{ left:0; }

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

/* ══════════════════════════════════════════════════════════════
   INTRO
   Hidden unless the head script decided this visit gets one, so a
   JS failure can never leave a blank sheet over the site.
   ══════════════════════════════════════════════════════════════ */
.intro{ display:none; }
.intro-on .intro{
  display:grid;
  place-items:center;
  position:fixed; inset:0;
  z-index:2000;                    /* over the grain (900) and the nav (100) */
  background:var(--paper);
  /* Failsafe: if script.js never runs, this clears itself anyway. */
  animation:introFailsafe .6s 3.4s forwards;
}
@keyframes introFailsafe{
  to{ opacity:0; visibility:hidden; pointer-events:none; }
}
/* Once JS has taken over, it owns the exit and the failsafe stands down. */
.intro-on .intro.is-managed{ animation:none; }
.intro-on .intro.is-leaving{
  opacity:0;
  transform:scale(1.04);
  transition:opacity .65s var(--ease-out), transform .65s var(--ease-out);
  pointer-events:none;
}

.intro__inner{ display:grid; justify-items:center; gap:2.4rem; }

.intro__grid{
  position:relative;
  width:5.5rem; height:5.5rem;
}
.intro__grid i{
  position:absolute; top:0; left:0;
  width:.34rem; height:.34rem;
  border-radius:1px;
  background:var(--chaos);
  opacity:0;
  will-change:transform;
}

.intro__word{
  margin:0;
  display:flex;
  font-family:var(--mono);
  font-weight:700;
  font-size:1rem;
  letter-spacing:.42em;
  text-indent:.42em;              /* balance the trailing letter-space */
  color:var(--ink);
}
.intro__word span{
  opacity:0;
  transform:translateY(.25rem);
  transition:opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.intro__word span.is-in{ opacity:1; transform:none; }

/* ══════════════════════════════════════════════════════════════
   HEADINGS

   opsz is pinned below the rendered size on purpose. In Fraunces a
   LOW optical size is the sturdy text cut and a high one is the fine
   display cut — so pinning it low keeps stroke weight visible at
   every size instead of letting the hairlines thin out as the type
   grows. Weight sits at 600, never 400.
   ══════════════════════════════════════════════════════════════ */
.hero__title,
.contact__title{
  font-family:var(--display);
  font-weight:600;
  font-optical-sizing:none;
  font-variation-settings:'opsz' 60;
}
.section__title{
  font-family:var(--display);
  font-weight:600;
  font-optical-sizing:none;
  font-variation-settings:'opsz' 40;
}
/* The rule: serif only where the type never renders below 28px.
   Case headings start at ~26px and grow, so they keep it. */
.case h3{
  font-family:var(--display);
  font-weight:600;
  font-optical-sizing:none;
  font-variation-settings:'opsz' 28;
}

/* Everything smaller is sans at 600. Below ~28px the serif stops
   earning its keep and starts costing legibility — and the sans is
   doing all the reading work on this page anyway. */
.faq summary,
.pin__card h3{
  font-family:var(--sans);
  font-weight:600;
  font-optical-sizing:auto;
  letter-spacing:-.012em;
}

/* ══════════════════════════════════════════════════════════════
   TYPE
   Tracking is size-specific — a single letter-spacing value is
   always wrong somewhere. Large display type gets negative tracking,
   small type a touch positive, body stays at 0. Leading moves
   inversely to size.
   ══════════════════════════════════════════════════════════════ */
.eyebrow{
  display:flex; align-items:center; gap:.5rem;
  font-family:var(--mono);
  font-weight:500;
  font-size:var(--fs-min);
  line-height:1.5;
  letter-spacing:.1em;              /* small + uppercase → open it up, but not to breaking */
  text-transform:uppercase;
  color:var(--ink-3);
  margin:0 0 1.35rem;
}
/* Hero eyebrow only. Right-aligned against the headline block, and
   carrying more weight now the accent dot has gone: per the type rule at
   the top of this file, a label that needs presence gets bolder and
   darker rather than bigger. */
.hero .eyebrow{
  justify-content:flex-end;
  text-align:right;
  font-weight:700;                  /* 700 is the heaviest mono weight loaded */
  font-size:var(--fs-min);
  color:var(--ink);
}
/* Tracked uppercase mono is wide, and this line has to stay on one row:
   step it up only as the content column can take it. */
@media (min-width:641px){  .hero .eyebrow{ font-size:var(--fs-sm); } }
@media (min-width:1024px){ .hero .eyebrow{ font-size:var(--fs-md); } }

.section__title{
  font-size:clamp(2.3rem, 1.4rem + 3.4vw, 3.9rem);
  line-height:1.05;                 /* large → tight */
  letter-spacing:-.02em;
  margin:0 0 1.1rem;
}

.section__intro{
  max-width:50ch;
  color:var(--ink-2);
  font-size:1.1em;
  line-height:1.6;
}


/* ══════════════════════════════════════════════════════════════
   BUTTONS — feedback on press, not on release.
   Press-in is fast (response must feel instant); release eases back
   more slowly along the mirrored curve. Targets are ≥48px tall.
   ══════════════════════════════════════════════════════════════ */
.btn{
  display:inline-flex; align-items:center; gap:.6rem;
  font-size:var(--fs-md); font-weight:500;
  letter-spacing:-.004em;
  text-decoration:none;
  padding:1rem 1.65rem;
  border-radius:999px;
  border:1px solid transparent;
  transform:translateZ(0);
  transition:background .25s var(--ease-out),
             color .25s var(--ease-out),
             border-color .25s var(--ease-out),
             transform .26s var(--ease-out);
}
.btn:active{
  transform:scale(.97);
  transition-duration:.09s;         /* down fast, up slow */
  transition-timing-function:var(--ease-in);
}
.btn svg{ width:1rem; height:1rem; transition:transform .25s var(--ease-out); }
.btn:hover svg{ transform:translateX(3px); }

.btn--solid{ background:var(--ink); color:var(--paper); }
.btn--solid:hover{ background:var(--chaos-2); }

.btn--ghost{ border-color:var(--line-2); color:var(--ink); }
.btn--ghost:hover{ border-color:var(--order); color:var(--order-2); background:var(--order-soft); }

/* ══════════════════════════════════════════════════════════════
   NAV — translucent material, content passes underneath
   ══════════════════════════════════════════════════════════════ */
.nav{
  position:sticky; top:0; z-index:100;
  background:rgba(247, 245, 240, .74);                 /* fallback */
  background:var(--material-bg);
  backdrop-filter:blur(var(--blur)) saturate(180%);
  -webkit-backdrop-filter:blur(var(--blur)) saturate(180%);
  border-bottom:1px solid transparent;                 /* stays transparent — see ::after */
}

/* Scroll edge effect instead of a hard divider: a short gradient that
   fades content out under the floating chrome, appearing only once
   there is content passing beneath it. */
.nav::after{
  content:"";
  position:absolute; left:0; right:0; top:100%;
  height:1.5rem;
  z-index:-1;
  background:linear-gradient(to bottom, var(--paper) 0%, transparent 100%);
  opacity:0;
  transition:opacity .35s var(--ease-out);
  pointer-events:none;
}
.nav.is-stuck::after{ opacity:.92; }

/* A pale translucent bar over a near-black section reads as a grey
   slab, so the chrome inverts while it is over one. Toggled from
   script.js by testing what is actually behind the bar. */
.nav,
.nav .logo__mark,
.nav .nav__links a,
.nav__cta,
.nav__toggle,
.nav__toggle span{
  transition:background .45s var(--ease-out), color .45s var(--ease-out),
             border-color .45s var(--ease-out);
}
.nav.is-over-dark{
  background:rgba(20,22,26,.62);
  border-bottom-color:transparent;
}
.nav.is-over-dark::after{ background:linear-gradient(to bottom, var(--ink) 0%, transparent 100%); }
.nav.is-over-dark .logo__word{ color:var(--paper); }
.nav.is-over-dark .logo__mark{ color:#74B4B6; }
.nav.is-over-dark .nav__links a{ color:#B9BEC3; }
.nav.is-over-dark .nav__links a:hover,
.nav.is-over-dark .nav__links a[aria-current="true"]{ color:var(--paper); }
.nav.is-over-dark .nav__indicator{ background:rgba(255,255,255,.14); }
.nav.is-over-dark .nav__cta{ background:var(--paper); color:var(--ink) !important; }
.nav.is-over-dark .nav__cta:hover{ background:#74B4B6; color:var(--ink) !important; }
.nav.is-over-dark .nav__toggle{ border-color:rgba(255,255,255,.32); }
.nav.is-over-dark .nav__toggle span{ background:var(--paper); }

.nav__inner{
  display:flex; align-items:center; justify-content:space-between;
  height:var(--nav-h);
}

.logo{
  display:inline-flex; align-items:center; gap:.6rem;
  text-decoration:none;
  transition:transform .26s var(--ease-out);
}
.logo:active{ transform:scale(.96); transition-duration:.09s; }
.logo__mark{ width:1.75rem; height:1.75rem; color:var(--order); flex:none; }
.logo__word{
  font-family:var(--mono);
  font-weight:700;
  font-size:1.05rem;
  letter-spacing:.24em;
  padding-left:.1em;
}

.nav__links{
  position:relative;
  display:flex; align-items:center; gap:1.85rem;
}

/* The travelling pill. Position and width are driven by two independent
   springs in script.js — a single spring across both desyncs. */
.nav__indicator{
  position:absolute;
  left:0; top:50%;
  width:0; height:2.25rem;
  border-radius:999px;
  background:var(--order-soft);
  transform:translate3d(0,-50%,0);
  opacity:0;
  pointer-events:none;
  will-change:transform;
}

.nav__links a{
  position:relative;
  z-index:1;
  text-decoration:none;
  font-size:1rem;
  letter-spacing:-.002em;
  color:var(--ink-2);
  transition:color .25s var(--ease-out), transform .26s var(--ease-out);
}
.nav__links a:hover{ color:var(--ink); }
.nav__links a:active{ transform:scale(.96); transition-duration:.09s; }
.nav__links a[aria-current="true"]{ color:var(--order-2); font-weight:500; }

.nav__cta{
  background:var(--ink); color:var(--paper) !important;
  padding:.6rem 1.2rem; border-radius:999px;
  font-weight:500;
  transition:background .25s var(--ease-out), transform .26s var(--ease-out);
}
.nav__cta:hover{ background:var(--chaos-2); }

.nav__toggle{
  display:none;
  width:2.9rem; height:2.9rem;
  border:1px solid var(--line-2); border-radius:999px;
  padding:0;
  place-items:center;
  gap:.36rem;
  transition:transform .26s var(--ease-out), border-color .25s var(--ease-out);
}
.nav__toggle:active{ transform:scale(.93); transition-duration:.09s; }
.nav__toggle span{
  display:block; width:1.15rem; height:2px; background:var(--ink);
  border-radius:2px;
  transition:transform .34s var(--ease-out);
}
.nav__toggle[aria-expanded="true"]{ border-color:var(--chaos); }
.nav__toggle[aria-expanded="true"] span:first-child{ transform:translateY(.118rem) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:last-child{ transform:translateY(-.118rem) rotate(-45deg); }

.nav__grabber{ display:none; }

/* ── the menu sheet ────────────────────────────────────────── */
@media (max-width:860px){
  .nav__toggle{ display:grid; }
  .nav__indicator{ display:none; }

  .nav__links{
    position:absolute;
    top:calc(100% + .5rem);
    left:var(--gutter); right:var(--gutter);
    flex-direction:column; align-items:stretch; gap:0;
    padding:.25rem 1.35rem 1.2rem;

    /* material, not a panel */
    background:var(--material-bg);
    backdrop-filter:blur(var(--blur)) saturate(180%);
    -webkit-backdrop-filter:blur(var(--blur)) saturate(180%);
    border:1px solid var(--material-edge);
    border-radius:1.15rem;
    box-shadow:0 1.5rem 3rem -1rem rgba(20,22,26,.22),
               0 .25rem .75rem -.25rem rgba(20,22,26,.10);

    /* Anchored to the button that opened it, so it grows out of its
       source and collapses back into it. */
    transform-origin:top right;

    /* Opening/closing is spring-driven from JS. Starting state only. */
    opacity:0;
    visibility:hidden;
    transform:translate3d(0,-.9rem,0) scale(.96);

    touch-action:none;              /* the sheet owns vertical drag */
    -webkit-user-select:none;
    user-select:none;
  }
  .nav__links.is-live{ visibility:visible; }

  .nav__links a{
    padding:1.05rem .25rem;         /* comfortably past a 48px target */
    font-size:1.15rem;
    letter-spacing:-.006em;
    border-bottom:1px solid var(--line);
  }
  .nav__links a:last-of-type{ border-bottom:none; }

  .nav__cta{
    margin-top:1.1rem;
    text-align:center;
    padding-block:.95rem;
  }

  .nav__grabber{
    display:block;
    width:2.5rem; height:.25rem;
    margin:.95rem auto -.4rem;
    border-radius:999px;
    background:var(--line-2);
  }
}

/* ══════════════════════════════════════════════════════════════
   HERO — the entropy field
   ══════════════════════════════════════════════════════════════ */
/* The first screen is mostly air on purpose. Content sits a little
   above centre and occupies roughly the upper-left third; the rest is
   left empty so the field can read as weather rather than decoration. */
.hero{
  position:relative;
  display:grid;
  /* Two rows: the headline block takes the space and centres itself in
     it, the proof sits on the floor. `align-content:center` would
     bunch both together in the middle and leave the floor empty. */
  grid-template-rows:1fr auto;
  /* No rem cap: the cap held the hero to 736px, so on a tall screen it
     stopped filling the view and let the dark section intrude on the
     first screen. The proof row is the floor of the grid, so whatever
     slack this leaves goes above it, not below. */
  min-height:88svh;
  padding:clamp(1.75rem, 5svh, 5rem) 0 clamp(1.25rem, 2.5svh, 2.25rem);
  overflow:hidden;
  /* Sky. A single wide, very low-contrast wash behind the peaks, offset
     to the right so the light appears to come from beyond the range.
     This is what stops the upper half reading as blank paper. */
  background:
    radial-gradient(115% 85% at 76% 34%,
      color-mix(in srgb, var(--order) 11%, transparent) 0%,
      color-mix(in srgb, var(--order) 4%,  transparent) 45%,
      transparent 68%);
}

/* Mist: the field doesn't stop, it dissolves upward into the paper. */
.hero::after{
  content:"";
  position:absolute; inset:auto 0 0; height:55%;
  z-index:2;
  pointer-events:none;
  background:linear-gradient(to top,
    color-mix(in srgb, var(--paper) 55%, transparent) 0%,
    transparent 70%);
}

/* ── The range ──────────────────────────────────────────────────
   Furthest is palest. The mask fades every silhouette out towards
   its summit, which is the haze a real distance puts between you
   and a ridgeline — and it means the peaks never draw a hard edge
   across the headline's airspace. */
.range{
  position:absolute; inset:0;
  width:100%; height:100%;
  z-index:0;
  pointer-events:none;
  -webkit-mask-image:linear-gradient(to bottom, transparent 8%, rgba(0,0,0,.5) 33%, #000 64%);
  mask-image:linear-gradient(to bottom, transparent 8%, rgba(0,0,0,.5) 33%, #000 64%);
}
.range path{ fill:var(--order); }
.range__far { opacity:.13; }
.range__mid { opacity:.19; }
/* The nearest silhouette is deliberately the palest of the solid fills.
   It carries only the body of the range; its surface is the particle
   field, which is cut along this exact outline in script.js and lands
   on top of it. Solid + dots composite to ~.31 — heavier than the mid
   ridge at .19, so the depth order still reads correctly. */
.range__near{ opacity:.12; }

@media (max-width:860px){
  /* Narrow screens give the copy the full width, so the range drops to
     the lower third and stays out from behind the words entirely. */
  .range{ inset:auto 0 0; height:62%; }
}

.hero__scroll{
  display:flex; align-items:center; gap:.7rem;
  flex:none;
  margin:0;
  font-family:var(--mono);
  font-size:var(--fs-min);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--ink-3);
}
.hero__scroll span{
  width:1.9rem; height:1px;
  background:currentColor;
  transform-origin:left;
  animation:reach 2.8s var(--ease-out) infinite;
}
@keyframes reach{
  0%,100%{ transform:scaleX(1); opacity:.6; }
  45%    { transform:scaleX(1.8); opacity:1; }
}

/* Sits behind the words, masked away from the top-left where the
   headline lives — the field must never fight the words for
   legibility. On narrow screens the text takes the full width, so
   the mask pushes the field down instead of across. */
/* Masked to a low band along the bottom so the blocks read as a range
   emerging from mist rather than a wash behind the words. The upper
   two-thirds of the screen stays genuinely empty. */
.field{
  position:absolute; inset:0;
  width:100%; height:100%;
  z-index:1;                          /* the nearest layer — in front of the range */
  pointer-events:none;
  opacity:.85;
  /* The first gradient is the SAME haze curve as .range, so the dots and
     the silhouettes they sit on fade into the paper together — one
     atmosphere, not two. The second only holds the dots back a little
     over the text column, for the moments they are scattered across the
     whole frame; the ridge outline keeps that corner empty at rest. */
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 8%, rgba(0,0,0,.5) 33%, #000 64%),
    linear-gradient(to right, rgba(0,0,0,.28) 0%, #000 42%);
  mask-image:
    linear-gradient(to bottom, transparent 8%, rgba(0,0,0,.5) 33%, #000 64%),
    linear-gradient(to right, rgba(0,0,0,.28) 0%, #000 42%);
  -webkit-mask-composite:source-in;
  mask-composite:intersect;
}
@media (max-width:860px){
  /* Full-width text on narrow screens, so only the vertical haze applies */
  .field{
    -webkit-mask-image:linear-gradient(to bottom, transparent 30%, rgba(0,0,0,.5) 48%, #000 72%);
    mask-image:linear-gradient(to bottom, transparent 30%, rgba(0,0,0,.5) 48%, #000 72%);
  }
}

.hero__inner{ position:relative; z-index:3; align-self:center; }

/* The size term carries BOTH axes. Width alone sized the headline at
   140px/line on every desktop, which made the hero 888px tall whatever
   the screen — and pushed the proof row off the bottom of any laptop.
   `min(9.8vw, 13svh)` lets whichever dimension is scarcer set the size,
   so the hero fits the viewport continuously instead of at breakpoints. */
.hero__title{
  font-size:clamp(3.4rem, min(1.3rem + 9.8vw, 13svh), 8.75rem);
  line-height:.94;                  /* biggest type on the page → tightest leading */
  letter-spacing:-.026em;
  margin:0 0 clamp(1rem, 2.5svh, 2.25rem);
}

.lede{
  max-width:42ch;
  font-size:clamp(1.15rem, min(1.08rem + .5vw, 2.6svh), 1.5rem);
  line-height:1.55;
  letter-spacing:-.006em;
  color:var(--ink-2);
  margin-bottom:clamp(1.1rem, 3svh, 2.4rem);
}

.hero__cta{ display:flex; flex-wrap:wrap; gap:.85rem; }


/* The hero's floor: proof on the left, the way out on the right,
   sharing one hairline. */
.hero__foot{
  position:relative; z-index:3;
  display:flex; flex-wrap:wrap; align-items:baseline;
  justify-content:space-between;
  gap:.9rem clamp(1.25rem, 2.5vw, 2rem);
  padding-top:clamp(.85rem, 1.8svh, 1.5rem);
  margin-top:clamp(1.1rem, 3.5svh, 3.5rem);
  border-top:1px solid var(--line);
}
/* One line, always. Left to itself the cue wraps onto a line of its
   own and reads as an orphan, so the proof list absorbs the wrapping
   and the cue holds the right edge. */
@media (min-width:861px){
  .hero__foot{ flex-wrap:nowrap; }
  .hero__proof{ flex:1 1 auto; min-width:0; }
}
@media (max-width:640px){
  /* the cue is the first thing to go when there's no room for both */
  .hero__foot .hero__scroll{ display:none; }
}
.hero__proof{
  display:flex; flex-wrap:wrap; align-items:center;
  gap:.6rem clamp(1.5rem, 4vw, 2.75rem);
  font-size:var(--fs-md);
  line-height:1.6;
  color:var(--ink-2);
}
.hero__proof b{ color:var(--ink); font-weight:600; font-variant-numeric:lining-nums; }

/* A small phone (390×667 and similar) is the one case where headline,
   lede, two buttons and three stats genuinely do not fit a screen. The
   gaps and the stat strip give way rather than any of the content —
   nothing here drops below 15px, and body copy stays at 18. */
@media (max-width:640px) and (max-height:740px){
  .hero{ min-height:96svh; padding-top:1.1rem; padding-bottom:.6rem; }
  .hero__title{ font-size:clamp(2.9rem, 10.5vw, 3.4rem); }
  .lede{ margin-bottom:1rem; }
  .hero__cta{ gap:.6rem; }
  .hero__foot{ margin-top:.75rem; padding-top:.6rem; }
  .hero__proof{ gap:.3rem .9rem; font-size:var(--fs-sm); }
  .lede{ line-height:1.45; }
  .hero__cta .btn{ padding:.8rem 1.15rem; }
}

/* ══════════════════════════════════════════════════════════════
   SECTIONS
   ══════════════════════════════════════════════════════════════ */
.section{ position:relative; padding:var(--section) 0; overflow:hidden; }
.section--alt{
  background:var(--paper-2);
  border-block:1px solid var(--line);
}
/* `overflow:hidden` on .section makes it a scroll container, which
   silently disables position:sticky anywhere inside it. Sections that
   hold a sticky element have to opt out. */
.section--flow{ overflow:visible; }
.section__head{ margin-bottom:clamp(2rem, 4vw, 3.25rem); max-width:44ch; }

.section > .wrap{ position:relative; z-index:1; }

/* ── work ──────────────────────────────────────────────────── */
/* The numbers were always the argument. They carry the section now,
   at display size, with one line of context above them — three
   paragraphs of narrative said the same thing three times. */
.cases{ display:grid; gap:clamp(2rem, 4vw, 3rem); }
.case{
  position:relative;
  display:grid;
  gap:clamp(1.5rem, 3vw, 2.5rem);
  padding:clamp(1.6rem, 3vw, 2.4rem);
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:1rem;
  transition:border-color .3s var(--ease-out);
}
@media (min-width:901px){
  /* Top-aligned, not centred. The body carries three story beats now
     rather than two lines, so a vertically centred schematic floats in
     the middle of the card with nothing either side of it. Aligned to
     the top it reads as a plate set beside the opening claim, which is
     the relationship it actually has to the text. */
  .case{ grid-template-columns:minmax(0, 1fr) minmax(0, 24rem); align-items:start; }
  .case__rig{ position:sticky; top:7rem; }
}
.case:hover{ border-color:var(--line-2); }

.case__head{
  display:grid;
  grid-template-columns:auto minmax(0, 1fr) auto;
  gap:.9rem;
  align-items:center;
  margin-bottom:1.1rem;
}
/* Not a client logo — theirs aren't mine to show. A glyph for what
   the system does. */
.case__mark{
  display:grid; place-items:center;
  width:2.6rem; height:2.6rem;
  border-radius:.7rem;
  background:var(--order-soft);
  color:var(--order-2);
}
.case__mark svg{ width:1.35rem; height:1.35rem; }
.case h3{
  font-size:clamp(1.45rem, 1.2rem + .9vw, 1.9rem);
  line-height:1.14;
  letter-spacing:-.016em;
}
.case__who{
  margin:.2rem 0 0;
  font-size:var(--fs-sm);
  color:var(--ink-3);
}
.case__status{
  align-self:start;
  white-space:nowrap;
  padding:.3rem .6rem;
  border-radius:2rem;
  background:var(--order-soft);
  color:var(--order-2);
  font-family:var(--mono);
  font-weight:700;
  font-size:var(--fs-min);
  letter-spacing:.04em;
}

/* On a phone the status pill and the client line can't share a row —
   the line gets squeezed into a four-word column. Drop the pill to its
   own row under the title instead. */
@media (max-width:640px){
  .case__head{ grid-template-columns:auto minmax(0, 1fr); row-gap:.7rem; }
  .case__mark{ grid-area:1 / 1; align-self:start; }   /* the title wraps to two lines here */
  .case__head > div{ grid-area:1 / 2; }
  .case__status{ grid-area:2 / 2; justify-self:start; }
}

.pills{ display:flex; flex-wrap:wrap; gap:.4rem; margin-bottom:1.1rem; }
.pills li{
  padding:.22rem .6rem;
  border:1px solid var(--line-2);
  border-radius:2rem;
  font-family:var(--mono);
  font-weight:500;
  font-size:var(--fs-min);
  letter-spacing:.02em;
  color:var(--ink-3);
}

/* The one sentence a skimmer will read. Serif, because it's a claim
   and not a caption. */
.case__claim{
  font-family:var(--display);
  font-optical-sizing:none;
  font-variation-settings:'opsz' 28;
  font-weight:600;
  font-size:clamp(1.2rem, 1.05rem + .55vw, 1.5rem);
  line-height:1.24;
  letter-spacing:-.012em;
  margin:0 0 .7rem;
}
.case__line{
  color:var(--ink-2);
  max-width:62ch;
  margin:0;
  font-size:var(--fs-md);
  line-height:1.6;
}

/* ── the collapse ──────────────────────────────────────────────
   The helix stage is one viewport tall and the cards run past that
   once the story beats are in, so inside the deck the story folds
   away and the front card opens it on demand. Outside the deck
   nothing is height-constrained, so the button stays hidden and the
   story is simply there.

   Collapsed with grid-template-rows 0fr → 1fr rather than max-height:
   a max-height has to be guessed, and a guess that is too small
   clips the last line while one that is too large makes the close
   animation idle before it appears to start. 0fr → 1fr animates to
   the content's own height, whatever that turns out to be. */
.case__toggle{
  display:inline-flex;
  align-items:center;
  gap:.45rem;
  margin:1.35rem 0 0;
  padding:.5rem .85rem .5rem .95rem;
  background:transparent;
  border:1px solid var(--line-2);
  border-radius:2rem;
  font:inherit;
  font-size:var(--fs-sm);
  color:var(--ink);
  cursor:pointer;
  transition:border-color .25s var(--ease-out), background .25s var(--ease-out);
}
.case__toggle:hover{ border-color:var(--ink-3); background:var(--paper-3); }
/* Set on cards that have turned away from the front slot. Left visually
   unchanged on purpose: the card is already dimmed and blurred by the
   helix, so greying the button too would read as a broken control
   rather than as one belonging to a card that isn't in front. */
.case__toggle:disabled{ cursor:default; }
.case__chev{
  width:.85rem; height:.85rem;
  transition:transform .35s var(--ease-out);
}
.case__toggle[aria-expanded="true"] .case__chev{ transform:rotate(180deg); }
/* One button, two labels — the visible one is chosen by state, so the
   accessible name changes with the control rather than staying frozen
   at whatever it said when the page loaded. */
.case__toggle[aria-expanded="false"] .case__toggle-off,
.case__toggle[aria-expanded="true"]  .case__toggle-on{ display:none; }

.case__storywrap{
  display:grid;
  grid-template-rows:1fr;
  transition:grid-template-rows .48s var(--ease-out);
}
.case__storyinner{ min-height:0; overflow:hidden; }
.case.is-shut .case__storywrap{ grid-template-rows:0fr; }

/* Inside the deck the CARD is the scroller, not the story. Capping the
   story was the first attempt and the numbers do not allow it: a folded
   card is already ~700px against an 800px stage, so the story's whole
   budget would have been about forty pixels. Capping the card instead
   costs nothing while folded — it fits under the cap — and when the
   reader opens the story the card scrolls rather than growing off the
   top and bottom of the stage.

   SCOPED TO THE OPEN CARD, and that scoping is load-bearing. Applied to
   every card in the deck, `overflow-y` makes each one a scroll
   container, and a scroll container is rasterised to its scrollport
   BEFORE the helix turns it: the perspective flattens out and the
   schematic gets cropped in mid-air, which is the effect breaking, not
   a detail. A folded card fits under the stage on its own, so there is
   nothing for the cap to do until the story is open. dkPlace() folds a
   card as it leaves the front slot, so at most one card is ever a
   scroller, and only ever the one squarely in front.

   Scroll chaining is deliberately left at the default: read to the end
   of the story and the page picks up again, advancing the deck. The
   `contain` that would keep the wheel inside the card reads as a trap,
   because the only way out is to move the pointer off the card. */
.deck.is-decked .case.is-open{
  max-height:92svh;
  overflow-y:auto;
}
/* The sticky schematic belongs to the flow layout, where the card is
   taller than the screen. In the deck the card is its own scroller and
   a sticky child inside it just detaches from the text it illustrates. */
.deck.is-decked .case__rig{ position:static; }

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

/* ── the story ─────────────────────────────────────────────────
   Three beats under the claim: what it was like before, what I did,
   and the part that nearly went wrong. A case study that reports only
   outcomes reads as a brochure; the beat that admits a difficulty is
   the one that reads as a person who was actually there.

   Set as a rule-led list rather than boxes — these sit INSIDE a card
   already, and a box inside a box is a nesting the eye has to unpick.
   The left rule does the same grouping job for none of the weight. */
.case__story{
  display:grid;
  gap:1.35rem;
  margin:1.6rem 0 0;
  padding-left:1.15rem;
  border-left:2px solid var(--line);
  max-width:64ch;
}
.beat h4{
  font-family:var(--mono);
  font-size:var(--fs-min);
  font-weight:700;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--ink-3);
  margin:0 0 .35rem;
}
.beat p{
  margin:0;
  color:var(--ink-2);
  font-size:var(--fs-sm);
  line-height:1.65;
}
/* Under the stats: how long it took and whether it is still up. The
   two facts a buyer wants that a stat row never carries. */
.case__meta{
  margin:1.1rem 0 0;
  font-family:var(--mono);
  font-size:var(--fs-min);
  letter-spacing:.04em;
  color:var(--ink-3);
}

/* ── the schematic ─────────────────────────────────────────────
   The slot a product page fills with a screenshot. There is no
   screenshot to put here: these are internal systems holding client
   records under confidentiality. An abstract of what the system does
   is the honest occupant, and the caption says so out loud — a
   missing image reads as evasion, a labelled diagram doesn't. */
.case__rig{ margin:0; }
/* The schematic is a dark instrument panel inset into the light card. Screens
   are withheld, so the drawing has to carry the authority a screenshot would —
   it reads louder than the paper stock around it on purpose. */
.case__rig svg{
  display:block;
  width:100%; height:auto;
  background:#0F1317;
  border:1px solid #232B33;
  border-radius:.75rem;
  overflow:hidden;
}
.case__rig figcaption{
  margin-top:.6rem;
  font-family:var(--mono);
  font-size:var(--fs-min);
  line-height:1.45;
  color:var(--ink-3);
  text-align:center;
}
/* Drawing grammar, shared by all three schematics. `vector-effect` keeps every
   stroke a true hairline no matter how far the viewBox is scaled down — without
   it the whole drawing thickens into mush on a phone. */
.rig .grid{ stroke:#1B232B; }
.rig .hair{ fill:none; stroke:#7C8B96; stroke-width:1; vector-effect:non-scaling-stroke; }
.rig .rule{ fill:none; stroke:#39454F; stroke-width:.75; vector-effect:non-scaling-stroke; }
.rig .reg { fill:none; stroke:#5A6B77; stroke-width:1; vector-effect:non-scaling-stroke; }
.rig .fillbox{ fill:#161C22; stroke:#46545F; stroke-width:1; vector-effect:non-scaling-stroke; }
/* 11px is a drawing dimension inside the schematic's fixed geometry, not
   UI chrome: at --fs-min the corner labels crowd the registration marks. */
.rig .mono{ font-family:var(--mono); font-size:11px; letter-spacing:.14em; fill:#78848D; }

/* the unresolved side runs warm, the built side runs cool — the same two
   registers the rest of the page uses for chaos and order */
.rig .old .fillbox{ fill:#191818; stroke:#8A5F49; }
.rig .old .rule   { stroke:#563A2C; }
.rig .new .fillbox{ fill:#141B20; stroke:#5E8EA3; }
.rig .new .rule   { stroke:#3E626F; }
.rig .accent  { stroke:#E4834F; }
.rig .accent-f{ fill:#E4834F; }

/* Parallax. Layers ride the pointer at their own depth, eased so they lag and
   settle rather than tracking the cursor rigidly. Translations are in SVG user
   units, so the effect scales with the drawing instead of overwhelming it when
   the card is small. JS sets the transform; this only says how it arrives. */
.rig .lyr{ transition:transform .5s cubic-bezier(.33,1,.68,1); }
@media (prefers-reduced-motion: reduce){
  .rig .lyr{ transition:none; }
}

.case__stats{
  display:grid;
  /* Content-sized, not three equal thirds. A figure like 145,000 is
     wider than a third of the row and an equal-thirds track has no
     give, so it ran into the figure beside it. */
  grid-template-columns:repeat(3, auto);
  justify-content:space-between;
  gap:clamp(1rem, 3vw, 2.25rem);
  margin:clamp(1.5rem, 3vw, 2.1rem) 0 0;
  padding-top:clamp(1.25rem, 2.5vw, 1.75rem);
  border-top:1px solid var(--line);
}
/* Markup is dt-then-dd, which is what a <dl> requires and what a
   screen reader wants to hear ("records moved — 145,000"). Sighted
   order is the other way round, so the figure is ordered above. */
.stat{ min-width:0; display:flex; flex-direction:column; }
.stat__lab{ order:2; }
.stat__fig{
  margin:0;
  font-family:var(--display);
  font-optical-sizing:none;
  font-variation-settings:'opsz' 40;
  font-weight:600;
  font-size:clamp(2.1rem, 1.3rem + 2.4vw, 3.4rem);
  line-height:.95;
  letter-spacing:-.03em;
  font-variant-numeric:lining-nums tabular-nums;
  color:var(--order-2);
}
/* Units ride small and warm so the numeral keeps the weight */
.stat__fig i{
  font-style:normal;
  font-size:.5em;
  letter-spacing:0;
  color:var(--chaos-2);
  margin-left:.06em;
}
.stat__lab{
  margin-top:.6rem;
  font-size:var(--fs-min);
  line-height:1.4;
  color:var(--ink-3);
  text-wrap:pretty;
}

@media (max-width:640px){
  /* Beside its label rather than above it — three stacked pairs run
     to most of a phone screen. `display:contents` dissolves the .stat
     wrapper so all six cells share ONE grid: every figure lands in the
     same right-hand column and they align down the edge, which a
     per-stat grid can't do (145,000 and 0 are nowhere near the same
     width). Right-aligned + tabular figures, like a statement. */
  .case__stats{
    grid-template-columns:1fr auto;
    column-gap:1.25rem;
    row-gap:.9rem;
    align-items:center;
  }
  .stat{ display:contents; }
  .stat__fig{ font-size:1.85rem; text-align:right; }
  /* order:0, NOT the flex-column ordering used at desktop — with the
     wrapper dissolved, order:2 would push all three labels past all
     three figures and shuffle the pairs. */
  .stat__lab{ order:0; margin-top:0; }
}

/* ── the deck ──────────────────────────────────────────────────
   Enhancement only. Everything above is the base case: three cards
   in an ordinary column, which is what ships without JS, under
   reduced motion, and on anything narrower than 901px. `.is-decked`
   is added by script.js only once it can undo itself.

   The cards then ride a vertical helix that scroll turns: each one
   swings up out of the lower left, squares up to the reader at the
   front — sharp and lit — and carries on out to the upper right,
   turning away, dimming toward the section's own ground and
   blurring out. script.js owns the geometry; everything here is the
   stage it happens on.

   Driven by NATIVE scroll against a sticky stage. The reference for
   this effect captures the wheel and re-emits it; that buys nothing
   here and costs find-in-page, trackpad momentum and the reader's
   sense that the page is theirs. Same picture, no hijack.
   ══════════════════════════════════════════════════════════════ */
.deck.is-decked{ padding-bottom:0; overflow:visible; }
.deck.is-decked .deck__stage{
  position:sticky; top:0;
  height:100svh;
  display:grid; align-content:center;
}
/* NOT a shared preserve-3d context. The obvious build puts
   `perspective` on the stage and `transform-style:preserve-3d` on
   everything down to the cards, and it goes wrong twice: the
   perspective property only reaches its own children, so it never
   arrives at a card three levels down, and preserve-3d turns the
   wrap into a containing block, which drags the position indicator
   off the bottom of the stage.

   Each card carries its own `perspective()` inside its transform
   instead. The vanishing point is the card's own centre, which IS
   the centre of the stage, because that is where every card sits
   before the helix moves it — so the cards agree on one vanishing
   point without sharing a 3D context. Paint order is then plain
   z-index, set from depth by script.js. */
.deck.is-decked .cases{
  display:block;
  position:relative;
  height:0;                 /* every card is absolute; the box is just an anchor */
}
.deck.is-decked .case{
  position:absolute;
  inset-inline:0;
  top:50%;
  /* Narrower than the column it comes from. A card the full width of
     the wrap leaves the helix no room to turn in: its neighbours
     arrive already on top of it, and the effect reads as clutter
     rather than as depth. */
  max-width:min(100%, 62rem);
  margin-inline:auto;
  opacity:0;
  pointer-events:none;
  /* the card turns about its own centre — the axis of the helix is
     carried by the translate, not by the origin */
  transform-origin:50% 50%;
  backface-visibility:hidden;
  will-change:opacity, transform;
  /* the reveal transition would fight the rAF that owns these — see
     the override below, which is what actually enforces this */
  transition:none;
  /* A turning card needs to sit off the page rather than on it, or
     the depth reads as scale alone. The shadow is keyed to the
     paper, not to black. */
  box-shadow:0 2.5rem 5rem -2rem rgba(20, 22, 26, .16);
}
/* The line above cannot do its job alone. `.js .case.reveal` carries a
   1.05s transform transition, it is also (0,3,0), and it lands later in
   this file — so at equal specificity it wins, and the deck's
   `transition:none` is silently dropped. A second-long transition
   against a transform rewritten every frame means a card never reaches
   the pose it was given: it trails the scroll by about a second, so
   what the reader sees is a card at the wrong point on the arc while
   its neighbour is somewhere else entirely. The helix stops reading as
   one turning object. This rule outranks the reveal at (0,4,0) and is
   the one that actually holds.

   The reveal itself is not the enemy — it is the right treatment where
   the deck is OFF, which is why it is overridden here rather than
   weakened at its source. */
.deck.is-decked .case.reveal{ transition:none; }

/* Receding cards fade toward the section's ground rather than toward
   black — this page is paper, and a black scrim on paper reads as a
   modal, not as distance. */
.deck.is-decked .case::after{
  content:"";
  position:absolute; inset:0; z-index:3;
  border-radius:1rem;
  background:var(--paper-2);
  opacity:var(--card-dim, 0);
  pointer-events:none;
}
.deck.is-decked .case__body,
.deck.is-decked .case__rig{ filter:blur(var(--card-blur, 0)); }

/* Depth cue for the reader: which of the three they're on. */
.deck__dots{ display:none; }
/* Absolute, not in flow: the .cases anchor has no height, so a
   flowed indicator would sit underneath the cards rather than below
   them. The stage is sticky, which is positioned, so it's the
   containing block. */
.deck.is-decked .deck__dots{
  display:flex; gap:.5rem;
  justify-content:center;
  position:absolute;
  left:0; right:0;
  bottom:clamp(1.75rem, 5vh, 3.5rem);
  margin:0;
}
.deck__dots i{
  width:.45rem; height:.45rem;
  border-radius:50%;
  background:var(--line-2);
  transition:background .3s var(--ease-out), transform .3s var(--ease-out);
}
.deck__dots i.is-on{ background:var(--order); transform:scale(1.35); }

/* ── the terms table ───────────────────────────────────────────
   Terms, not figures. The promise the page makes is that the price
   is agreed before anything is built — not that it is published — so
   this states the terms a reader actually needs before a call. */
.price{
  display:grid;
  gap:0;
  margin:1.35rem 0 .85rem;
  border:1px solid var(--line);
  border-radius:.6rem;
  overflow:hidden;
  max-width:34rem;
}
.price > div{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:1rem;
  padding:.75rem .95rem;
  border-bottom:1px solid var(--line);
}
.price > div:last-child{ border-bottom:0; }
.price dt{
  font-size:var(--fs-sm);
  color:var(--ink-2);
}
.price dd{
  margin:0;
  font-family:var(--mono);
  font-size:.85rem;
  font-weight:700;
  color:var(--ink);
  text-align:right;
}
.price__note{
  font-size:var(--fs-sm);
  color:var(--ink-3);
  margin:0;
  max-width:52ch;
}

/* ══════════════════════════════════════════════════════════════
   WHO — the person behind the proprietorship
   ══════════════════════════════════════════════════════════════ */
/* The monogram is a placeholder that has to look deliberate rather
   than broken — an empty avatar ring reads as a failed image. Replace
   the span with an <img> and this styling drops away with it. *//* Scoped to the unclassed paragraphs only: .who__links is also a <p>,
   and `.who__body > p` outranks a bare class selector, so an unscoped
   rule here silently overrode its margin. *//* Unfilled hrefs are flagged rather than left to ship silently. */
/* ══════════════════════════════════════════════════════════════
   THE PINNED SEQUENCE

   Base state is an ordinary dark section with the four steps in a
   grid — that is what renders with no JS, under reduced motion, and
   in print. `.is-pinned` is added by script.js only when the scene
   can actually run, and everything below it is the enhancement.

   Four step colours, warm → cool, so the palette itself walks from
   disorder to order.
   ══════════════════════════════════════════════════════════════ */
.pin{
  --s1:#E8865E;   /* we talk        — warmest, most disordered */
  --s2:#DFAE5E;   /* priced first   */
  --s3:#74B4B6;   /* put to work    */
  --s4:#6FA3B8;   /* yours          — coolest, fully settled */

  position:relative;
  background:var(--ink);
  color:var(--paper);
  padding:var(--section) 0;
}
.pin__stage{
  position:relative;
  width:100%;
  max-width:var(--wrap);
  margin-inline:auto;
  padding-inline:var(--gutter);
}
.pin__scene{ display:none; }          /* only exists in the pinned state */

.eyebrow--dark{ color:#9AA0A6; }

.pin__title{
  font-family:var(--display);
  font-weight:600;
  font-optical-sizing:none;
  font-variation-settings:'opsz' 60;
  font-size:clamp(2.3rem, 1.4rem + 3.4vw, 3.9rem);
  line-height:1.05;
  letter-spacing:-.02em;
  margin:0 0 1.1rem;
}
.pin__lede{
  max-width:46ch;
  color:#B9BEC3;
  font-size:1.1em;
  line-height:1.6;
}
.pin__hint{ display:none; }
.pin__legend{ display:none; }
.pin__rail{ display:none; }

/* Explicit column counts — auto-fit lands on three across at desktop
   widths and leaves the fourth step stranded on its own row. */
.pin__cards{
  display:grid;
  grid-template-columns:1fr;
  gap:clamp(2rem, 4vw, 2.75rem);
  margin-top:clamp(3rem, 6vw, 4.5rem);
}
@media (min-width:600px){ .pin__cards{ grid-template-columns:1fr 1fr; } }
@media (min-width:1040px){ .pin__cards{ grid-template-columns:repeat(4, 1fr); } }
.pin__card{ position:relative; padding-top:1.9rem; border-top:3px solid rgba(255,255,255,.18); }
.pin__card[data-step="1"]{ border-top-color:var(--s1); }
.pin__card[data-step="2"]{ border-top-color:var(--s2); }
.pin__card[data-step="3"]{ border-top-color:var(--s3); }
.pin__card[data-step="4"]{ border-top-color:var(--s4); }

.pin__badge{
  font-family:var(--mono);
  font-weight:700;
  font-size:var(--fs-min);
  letter-spacing:.1em;
  color:#9AA0A6;
  display:block; margin-bottom:.7rem;
}
.pin__card h3{ font-size:1.6rem; line-height:1.16; margin-bottom:.7rem; }
.pin__card p{ color:#B9BEC3; font-size:var(--fs-md); line-height:1.62; margin:0; }
.pin__meta{
  margin-top:.9rem !important;
  font-family:var(--mono);
  font-size:var(--fs-min) !important;
  letter-spacing:.02em;
  color:#8A9096 !important;
}

.pin__outro{ margin-top:clamp(3rem, 6vw, 4.5rem); }
.btn--on-dark{
  border:1px solid rgba(255,255,255,.3);
  color:var(--paper);
}
.btn--on-dark:hover{ background:var(--paper); color:var(--ink); border-color:var(--paper); }

/* ── enhanced: the actual pin ──────────────────────────────── */
.pin.is-pinned{
  /* 3.6 viewports, not 6. The sequence used to be the tallest thing on
     the page — half the document for 140 words — which read as method
     standing in for evidence. Same four steps, same scene, less scroll
     tax. The chapter timings in script.js are normalised to progress,
     so they rescale with this number on their own; what changed there
     is the SHARE of the budget given to the intro (cut) and to the
     four cards (raised). */
  height:360vh;
  padding:0;
}
.pin.is-pinned .pin__stage{
  position:sticky; top:0;
  height:100vh; height:100svh;
  max-width:none;
  padding-inline:0;
  display:grid;
  place-items:center;
  overflow:hidden;
}
.pin.is-pinned .pin__scene{
  display:block;
  position:absolute; inset:0;
  width:100%; height:100%;
  z-index:0;
}

/* Every overlay shares one cell of the stage grid and is faded in and
   out by the scrubber. */
.pin.is-pinned .pin__intro,
.pin.is-pinned .pin__outro,
.pin.is-pinned .pin__cards{
  grid-area:1/1;
  z-index:2;
  width:min(var(--wrap), 100%);
  padding-inline:var(--gutter);
  margin:0;
  pointer-events:none;
}
.pin.is-pinned .pin__intro,
.pin.is-pinned .pin__outro{
  position:relative;
  text-align:center; justify-self:center;
  isolation:isolate;
}
/* The board runs behind these words, so they carry their own pool of
   darkness rather than relying on the blocks to be somewhere else. */
.pin.is-pinned .pin__intro::before,
.pin.is-pinned .pin__outro::before{
  content:"";
  position:absolute; inset:-60% -14%;
  z-index:-1;
  background:radial-gradient(closest-side, rgba(20,22,26,.94), rgba(20,22,26,.72) 55%, rgba(20,22,26,0) 100%);
  pointer-events:none;
}
.pin.is-pinned .pin__outro .btn{ pointer-events:auto; }
.pin.is-pinned .pin__lede{ margin-inline:auto; }

/* Cards stack in one place; only the active one is visible. */
.pin.is-pinned .pin__cards{
  display:block;
  position:relative;
}
.pin.is-pinned .pin__card{
  position:absolute;
  top:50%;
  right:var(--gutter);
  width:min(26rem, 42vw);
  transform:translateY(-50%);
  padding:1.75rem;
  border:1px solid rgba(255,255,255,.16);
  border-top-width:3px;
  border-radius:.9rem;
  /* Mostly opaque with a light blur. A heavy blur over the moving
     board smears the blocks into a coloured halo behind the text. */
  background:rgba(18,20,24,.93);
  backdrop-filter:blur(6px);
  -webkit-backdrop-filter:blur(6px);
  box-shadow:0 1.5rem 3rem -1rem rgba(0,0,0,.5);
  opacity:0;
}
@media (max-width:860px){
  .pin.is-pinned .pin__card{
    right:auto; left:var(--gutter);
    width:auto; max-width:none;
    inset-inline:var(--gutter);
    top:auto; bottom:5.5rem;
    transform:none;
  }
}

/* Bottom-left, clear of the legend's bottom label. */
.pin.is-pinned .pin__hint{
  display:block;
  position:absolute; bottom:2.9rem; left:var(--gutter);
  z-index:2;
  font-family:var(--mono);
  font-size:var(--fs-min);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:#8A9096;
  margin:0;
}
.pin.is-pinned .pin__hint span{ animation:nudge 2.4s var(--ease-out) infinite; display:inline-block; }
@keyframes nudge{ 0%,70%,100%{ transform:translateY(0); } 85%{ transform:translateY(3px); } }

/* Anchored corner labels — the wayfinding that stops the zoom being
   disorienting. */
/* Laid out as a diamond, not a rectangle: an isometric projection
   turns the four corners of a square board into top / right / bottom
   / left on screen, so the labels sit where their zone actually is. */
.pin.is-pinned .pin__legend{
  display:block;
  /* Top inset clears the sticky nav: the stage sticks to top:0, so the
     first 4.5rem of it sits behind the chrome and the top label was
     landing underneath it. Centre the diamond in the *visible* stage. */
  position:absolute;
  inset:calc(var(--nav-h) + 1rem) var(--gutter) 3.4rem;
  z-index:1;
  pointer-events:none;
}
.pin.is-pinned .pin__legend li{
  position:absolute;
  display:flex; align-items:center; gap:.5rem;
  isolation:isolate;
  font-family:var(--mono);
  font-size:var(--fs-min);
  letter-spacing:.06em;
  white-space:nowrap;
  color:#9AA0A6;                   /* 5.4:1 on the section ink */
  transition:color .4s var(--ease-out), opacity .4s var(--ease-out);
}
.pin.is-pinned .pin__legend li[data-step="1"]{ top:0; left:50%; transform:translateX(-50%); }
.pin.is-pinned .pin__legend li[data-step="2"]{ right:0; top:50%; transform:translateY(-50%); }
.pin.is-pinned .pin__legend li[data-step="3"]{ bottom:0; left:50%; transform:translateX(-50%); }
.pin.is-pinned .pin__legend li[data-step="4"]{ left:0; top:50%; transform:translateY(-50%); }
/* Same trick the intro and outro use: the board runs underneath these
   labels, so each one carries its own pool of darkness rather than
   relying on the blocks to be somewhere else. */
.pin.is-pinned .pin__legend li::before{
  content:"";
  position:absolute; inset:-.55rem -1.1rem;
  z-index:-1;
  background:radial-gradient(closest-side, rgba(20,22,26,.92), rgba(20,22,26,.66) 60%, rgba(20,22,26,0) 100%);
  pointer-events:none;
}
.pin.is-pinned .pin__legend i{
  width:.6rem; height:.6rem; flex:none;
  background:currentColor;
  transition:background .4s var(--ease-out);
}
/* Every label repeats its own card's heading verbatim — "Put to work",
   "Yours" — so highlighting the active one said the same thing twice, and
   the right-hand vertex is exactly where the card sits. The label steps
   aside instead: the card names the step you are on, the diamond shows the
   three you are not. Before the first card and after the last, nothing is
   active and all four are visible, which bookends the section. */
.pin.is-pinned .pin__legend li.is-on{ opacity:0; }

.pin.is-pinned .pin__rail{
  display:block;
  position:absolute; left:var(--gutter); right:var(--gutter); bottom:1.4rem;
  height:2px;
  background:rgba(255,255,255,.12);
  z-index:2;
}
.pin.is-pinned .pin__rail span{
  display:block; height:100%; width:0;
  background:var(--paper);
  transform-origin:left;
}
@media (max-width:860px){
  .pin.is-pinned .pin__legend{ display:none; }
  .pin.is-pinned .pin__intro,
  .pin.is-pinned .pin__outro{ align-self:start; padding-top:6rem; }
}

/* ── questions: a drawer of files ──────────────────────────────
   Tabs are the index and the folder below holds one file at a time.
   The reference for this is a physical drawer, so three things have
   to be true or it reads as browser chrome instead:

     1. The open tab JOINS the folder. Its bottom edge is erased and
        it is pulled 1px down over the folder's top border, so the two
        are one piece of card.
     2. The shoulders FLARE OUTWARD. Rounded top corners alone read as
        a browser tab; the outward curve at the base is the thing that
        says manila folder. Drawn with two pseudo-elements carrying an
        offset box-shadow — a solid corner with a quarter-circle bitten
        out of it.
     3. The rest of the drawer shows. Two edges peek below the open
        folder, so it is a stack you are looking into rather than a
        single card.
   ══════════════════════════════════════════════════════════════ */
.files{ position:relative; }

.files__tabs{
  display:flex;
  flex-wrap:wrap;
  gap:.5rem;
  padding-inline:.9rem;
  position:relative;
  z-index:2;
}
.files__tab{
  position:relative;
  padding:.7rem 1.1rem .8rem;
  border:1px solid var(--line-2);
  border-bottom:0;
  border-radius:.7rem .7rem 0 0;
  background:var(--paper-2);
  color:var(--ink-3);
  font-family:var(--mono);
  font-weight:700;
  font-size:var(--fs-min);
  letter-spacing:.08em;
  text-transform:uppercase;
  cursor:pointer;
  transition:background .25s var(--ease-out), color .25s var(--ease-out),
             transform .3s cubic-bezier(.33,1,.68,1);
}
.files__tab:hover{ color:var(--ink); background:var(--paper-3); }

/* The open file: joined to the folder, and lifted clear of the others. */
.files__tab[aria-selected="true"]{
  background:var(--paper);
  color:var(--order-2);
  transform:translateY(1px);      /* over the folder's own top border */
  z-index:1;
}
/* The outward flare, drawn only on the open tab. */
.files__tab[aria-selected="true"]::before,
.files__tab[aria-selected="true"]::after{
  content:"";
  position:absolute; bottom:-1px;
  width:.7rem; height:.7rem;
  background:transparent;
}
.files__tab[aria-selected="true"]::before{
  left:-.7rem;
  border-bottom-right-radius:.7rem;
  box-shadow:.24rem .24rem 0 0 var(--paper);
}
.files__tab[aria-selected="true"]::after{
  right:-.7rem;
  border-bottom-left-radius:.7rem;
  box-shadow:-.24rem .24rem 0 0 var(--paper);
}

.files__body{
  position:relative;
  z-index:1;
  overflow:hidden;
  /* Every panel shares one grid cell, so the folder is as tall as its
     tallest file and switching never jolts the page. */
  display:grid;
  background:var(--paper);
  border:1px solid var(--line-2);
  border-radius:.7rem;
  padding:clamp(1.5rem, 3.5vw, 2.5rem);
}
.files__panel{
  /* `start`, not the default stretch: a stretched grid item is as tall
     as the row, which is as tall as the LONGEST panel — so every panel
     would measure the same and the folder could never fit itself to
     the one that's open. */
  align-self:start;
  max-width:64ch;
}
/* `hidden` keeps six panels out of the way for the split second before
   script.js runs — and permanently in a reader that reports HTML but
   never executes the script. Neither case wants them gone: with JS the
   `.is-on` class does the showing, and without it all seven should
   read. So the attribute is overridden either way. */
.files__panel[hidden]{ display:block; }

/* Sharing one grid cell is what keeps the switch from jolting the page
   — but ONLY once something is managing which of them is visible.
   Without JS this would stack all seven on top of each other, so the
   overlap is scoped to `.js` and the plain document flows instead. */
.js .files__panel{ grid-area:1 / 1; }

/* No script: the tab strip controls nothing, so it goes, and the files
   read as an ordinary run of questions and answers. */
html:not(.js) .files__tabs,
html:not(.js) .files__edge{ display:none; }
html:not(.js) .files__panel + .files__panel{
  margin-top:1.5rem;
  padding-top:1.5rem;
  border-top:1px solid var(--line);
}

.files__q{
  font-family:var(--display);
  font-optical-sizing:none;
  font-variation-settings:'opsz' 28;
  font-weight:600;
  font-size:clamp(1.4rem, 1.15rem + .9vw, 1.95rem);
  line-height:1.16;
  letter-spacing:-.016em;
  margin-bottom:.85rem;
}
.files__panel p{
  margin:0;
  color:var(--ink-2);
  font-size:var(--fs-md);
  line-height:1.62;
}
.files__panel p + p{ margin-top:.9rem; }
.files__panel .faq__note{ font-size:var(--fs-sm); color:var(--ink-3); }

/* The rest of the drawer. */
.files__edge{
  display:block;
  height:.55rem;
  margin-inline:auto;
  border:1px solid var(--line-2);
  border-top:0;
  border-radius:0 0 .7rem .7rem;
  background:var(--paper-2);
}
.files__edge:nth-of-type(1){ width:calc(100% - 1.6rem); }
.files__edge:nth-of-type(2){ width:calc(100% - 3.4rem); opacity:.7; }

.files__ask{
  margin:clamp(1.25rem, 2.5vw, 1.75rem) 0 0;
  font-size:var(--fs-sm);
  color:var(--ink-3);
}
.files__ask a{
  color:var(--order-2);
  text-decoration:none;
  border-bottom:2px solid var(--line-2);
  padding-bottom:2px;
  transition:border-color .25s var(--ease-out), color .25s var(--ease-out);
}
.files__ask a:hover{ border-bottom-color:var(--chaos); color:var(--chaos-2); }

/* ── the switch ────────────────────────────────────────────────
   Enhancement only, and scoped to `.js`: with no script every panel
   is simply visible, one after another, which is a readable FAQ. */
/* Height is set by script.js from the open panel; the transition only
   engages once the first measurement has landed, so the folder never
   animates open from nothing on load. */
.js .files.is-live .files__body{
  transition:height .42s cubic-bezier(.33,1,.68,1);
}
.js .files__panel{
  opacity:0;
  visibility:hidden;
  transform:translateY(.5rem);
  transition:opacity .32s var(--ease-out), transform .42s cubic-bezier(.33,1,.68,1),
             visibility .32s;
}
.js .files__panel.is-on{
  opacity:1;
  visibility:visible;
  transform:none;
}
@media (prefers-reduced-motion:reduce){
  .js .files__panel{ transition:none; transform:none; }
  .js .files.is-live .files__body{ transition:none; }
  .files__tab{ transition:none; }
}

@media (max-width:640px){
  .files__tabs{ gap:.35rem; padding-inline:.5rem; }
  .files__tab{ padding:.55rem .75rem .65rem; letter-spacing:.05em; }
}

/* ── the anti-scope: the lattice ───────────────────────────────
   Same construction as the questions used to have — cells in a grid
   whose 1px gaps ARE the rules, one background showing through — but
   it belongs here. A grid draws the edges of the list, which is the
   whole point of this block: it is finite, and you can see that it is.

   Six cells: the heading and five refusals, 3×2 on desktop.

   The arrival is sequential rather than all at once. The rule draws
   down the side like a pen going down a list and the marks land under
   it as it passes — delays start at .18s and step by .1s. The gesture
   is the content.
   ══════════════════════════════════════════════════════════════ */
.wont{
  position:relative;
  margin-top:clamp(2.5rem, 5vw, 4rem);
  display:grid;
  grid-template-columns:1fr;
  gap:1px;
  background:var(--chaos);
  border:1px solid var(--chaos);
  border-radius:1rem;
  overflow:hidden;
}
@media (min-width:761px){ .wont{ grid-template-columns:repeat(3, 1fr); } }

.wont__cell{
  position:relative;
  background:var(--chaos-soft);
  padding:clamp(1.15rem, 2.4vw, 1.6rem);
}
.wont__cell p{
  margin:0;
  padding-left:1.7rem;
  color:var(--ink-2);
  font-size:var(--fs-md);
  line-height:1.55;
}
/* The mark sits in the cell, not in a list marker, so it can be
   animated on its own. */
.wont__cell p::before{
  content:"✗";
  position:absolute;
  left:clamp(1.15rem, 2.4vw, 1.6rem);
  color:var(--chaos-2);
  font-weight:700;
}
.wont__title{
  background:var(--chaos);
  display:flex;
  align-items:flex-end;
}
.wont__title h3{
  font-family:var(--mono);
  font-weight:700;
  font-size:var(--fs-min);
  letter-spacing:.1em;
  line-height:1.5;
  text-transform:uppercase;
  color:var(--paper);
}

/* ── the sequence ──────────────────────────────────────────────
   Every start state is scoped to `.js`, so without JavaScript the
   block is simply drawn and legible. `.is-in` arrives from the same
   IntersectionObserver that drives every other reveal on the page. */
.js .wont__cell{
  opacity:0;
  transform:translateY(.4rem);
  transition:opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.js .wont__cell p::before{
  transform:scale(.4) rotate(-35deg);
  transition:transform .45s var(--ease-out);
}
.js .wont.is-in .wont__cell{ opacity:1; transform:none; }
.js .wont.is-in .wont__cell p::before{ transform:none; }

.js .wont__cell:nth-child(1){ transition-delay:.06s; }
.js .wont__cell:nth-child(2),
.js .wont__cell:nth-child(2) p::before{ transition-delay:.18s; }
.js .wont__cell:nth-child(3),
.js .wont__cell:nth-child(3) p::before{ transition-delay:.28s; }
.js .wont__cell:nth-child(4),
.js .wont__cell:nth-child(4) p::before{ transition-delay:.38s; }
.js .wont__cell:nth-child(5),
.js .wont__cell:nth-child(5) p::before{ transition-delay:.48s; }
.js .wont__cell:nth-child(6),
.js .wont__cell:nth-child(6) p::before{ transition-delay:.58s; }

@media (prefers-reduced-motion:reduce){
  .js .wont__cell,
  .js .wont__cell p::before{ transition:none; transform:none; opacity:1; }
}

/* ══════════════════════════════════════════════════════════════
   CONTACT
   ══════════════════════════════════════════════════════════════ */
.contact{
  position:relative;
  padding-block:clamp(6rem, 12vw, 10rem);
  text-align:center;
}
.contact__inner{ position:relative; z-index:1; max-width:56rem; margin-inline:auto; }
/* .eyebrow is a flex row, so centring the section's text doesn't move it */
.contact .eyebrow{ justify-content:center; }

/* ── the closing frame ─────────────────────────────────────────
   The hero's range, mirrored and anchored to the floor. It rises
   into place when the section arrives instead of simply being
   there — a backdrop you scroll past reads as wallpaper; one that
   arrives reads as an ending. */
.range--close{
  inset:auto 0 0;
  height:min(78%, 30rem);
  z-index:0;
  transform:scaleX(-1);
  -webkit-mask-image:linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.5) 42%, rgba(0,0,0,.9) 86%);
  mask-image:linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.5) 42%, rgba(0,0,0,.9) 86%);
}
/* Lighter than the hero's copy. The hero puts type in its upper third
   and leaves the range the space below; here the words run down into
   it, so the bands have to sit further back. */
.range--close .range__far { opacity:.085; }
.range--close .range__mid { opacity:.115; }
.range--close .range__near{ opacity:.07; }

/* ── the swell ─────────────────────────────────────────────────
   The bands drift. Each one has its own period and its own throw,
   with the nearest moving furthest and fastest — the same parallax
   the aerial perspective already implies, so depth is stated twice
   and the three never line up into a single sliding sheet.

   Periods are mutual primes (23 / 31 / 43s) so the pattern doesn't
   visibly repeat: they only return to phase after about nine hours.
   Negative delays start each one mid-cycle rather than all three
   from a standstill.

   Transforms are in viewBox units, so the largest throw is 30/1440
   — around 2% of the width. Anything you can consciously track is
   too much for something meant to sit behind a sentence. */
@keyframes swell{
  0%,100%{ transform:translate3d(-1em, 0, 0); }
  50%    { transform:translate3d( 1em, -0.28em, 0); }
}
.range--close path{
  will-change:transform;
  animation:swell var(--swell-t) ease-in-out infinite;
  animation-delay:var(--swell-d);
}
/* 1em on an SVG child resolves against its own font-size, which is
   how each band gets a different throw from one keyframe rule. */
.range--close .range__far { font-size:14px; --swell-t:43s; --swell-d:-7s;  }
.range--close .range__mid { font-size:22px; --swell-t:31s; --swell-d:-19s; }
.range--close .range__near{ font-size:30px; --swell-t:23s; --swell-d:-3s;  }

@media (prefers-reduced-motion:reduce){
  .range--close path{ animation:none; }
}

@media (max-width:860px){
  /* A tall band on a narrow screen puts the whole closing paragraph
     on top of the near ridge. Keep it to the floor. */
  .range--close{ height:min(48%, 17rem); }
}
/* Both start hidden ONLY when script.js is there to bring them in —
   without it the section is simply present, as it should be. */
.js .range--close,
.js .contact__title{ opacity:0; }

.contact.is-arriving .range--close{
  animation:closeRise 2.2s cubic-bezier(.16,1,.3,1) both;
}
.contact.is-arriving .contact__title{
  animation:closeTitle 1.5s cubic-bezier(.16,1,.3,1) both;
}
@keyframes closeRise{
  from{ opacity:0; transform:scaleX(-1) translateY(15%) scale(1.05); }
  to  { opacity:1; transform:scaleX(-1) translateY(0)   scale(1); }
}
/* Out of focus into focus, not just up from below — the title
   resolves the same way everything else on this page does. */
@keyframes closeTitle{
  0%  { opacity:0; filter:blur(.5rem); }
  55% { opacity:1; }
  to  { opacity:1; filter:blur(0); }
}
@media (prefers-reduced-motion:reduce){
  .contact.is-arriving .range--close,
  .contact.is-arriving .contact__title{ animation:none; opacity:1; filter:none; }
}
.contact__title{
  font-size:clamp(2.7rem, 1.4rem + 5.8vw, 5.2rem);
  line-height:1;
  letter-spacing:-.024em;
  margin-bottom:1.6rem;
}
.contact__lede{
  max-width:42ch;
  margin-inline:auto;
  font-size:clamp(1.15rem, 1.05rem + .4vw, 1.35rem);
  line-height:1.55;
  letter-spacing:-.006em;
  color:var(--ink-2);
  margin-bottom:2.4rem;
}
.contact__actions{ display:flex; flex-wrap:wrap; gap:.85rem; justify-content:center; }
.contact__fine{
  margin-top:2.6rem;
  font-size:var(--fs-md);
  line-height:1.65;
  /* --ink-3 measured 4.39:1 here once the closing range was behind it,
     which is under the bar for 17px text. This is the one place on the
     page where fine print sits on a tinted ground. */
  color:var(--ink-2);
}

/* ══════════════════════════════════════════════════════════════
   FOOTER
   ══════════════════════════════════════════════════════════════ */
.footer{
  position:relative;
  background:var(--paper-2);
  border-top:1px solid var(--line);
  padding:clamp(3.5rem, 6vw, 5rem) 0 clamp(2.5rem, 5vw, 3.5rem);
  overflow:hidden;
}
.footer__water{
  position:absolute; inset:0 0 auto; height:7.5rem;
  pointer-events:none;
  mask-image:linear-gradient(to bottom, #000, transparent);
  -webkit-mask-image:linear-gradient(to bottom, #000, transparent);
}
.footer__water svg{ width:180%; height:100%; }
.footer__water .w{ fill:none; stroke:var(--order); stroke-width:1.25; stroke-linecap:round; }
/* Each path repeats every 800 user units, so a -800px drift loops
   seamlessly with no visible reset. */
.footer__water .w1{ opacity:.22; animation:drift 30s linear infinite; }
.footer__water .w2{ opacity:.13; animation:drift 44s linear infinite reverse; }

@keyframes drift{
  from{ transform:translateX(0); }
  to  { transform:translateX(-800px); }
}

.footer__inner{
  position:relative; z-index:1;
  display:flex; flex-wrap:wrap;
  gap:2.2rem clamp(2rem, 6vw, 4rem);
  justify-content:space-between;
  align-items:flex-start;
}
.footer__brand .logo__word{ display:block; margin-bottom:.7rem; }
.footer__expand{
  display:block;
  text-align:left;
  padding:0;
  font-family:var(--mono);
  font-weight:500;
  font-size:var(--fs-min);
  line-height:1.7;
  letter-spacing:.02em;
  color:var(--ink-3);
  border-bottom:1px dotted var(--line-2);
  transition:color .25s var(--ease-out), opacity .18s var(--ease-out);
}
.footer__expand:hover{ color:var(--chaos-2); }
.footer__expand.is-swapping{ opacity:0; }

.footer__nav{ display:flex; flex-wrap:wrap; gap:.5rem 1.6rem; }
.footer__nav a{
  font-size:var(--fs-md); color:var(--ink-2); text-decoration:none;
  transition:color .25s var(--ease-out), transform .26s var(--ease-out);
}
.footer__nav a:hover{ color:var(--chaos-2); }
.footer__nav a:active{ transform:scale(.96); transition-duration:.09s; }
.footer__legal{ font-size:var(--fs-sm); line-height:1.7; color:var(--ink-3); }
.footer__legal p{ margin:0 0 .3rem; }

/* ══════════════════════════════════════════════════════════════
   REVEAL — not gesture-driven, so a transition is the right tool
   ══════════════════════════════════════════════════════════════ */
.js .reveal{
  opacity:0;
  transform:translate3d(0,.9rem,0);
  transition:opacity .7s var(--ease-out), transform .7s var(--ease-out);
}
.js .reveal.is-in{ opacity:1; transform:none; }

/* The case cards get the helix's arrival instead of the generic rise.
   --enter-tf is written by script.js from dkAt(), so the start pose is
   the same curve the helix rode; the fallback is the ordinary reveal,
   which is what applies under reduced motion or if the property never
   arrives. Longer than the generic .7s because the move is bigger — a
   turn that lands as fast as a nudge reads as a glitch.

   NOTE ON SPECIFICITY: this is (0,3,0) and the reduced-motion override
   below is (0,2,0), so that block CANNOT reach this rule. It needs its
   own entry, and it has one. */
.js .case.reveal{
  transform:var(--enter-tf, translate3d(0,.9rem,0));
  transition:opacity .8s var(--ease-out), transform 1.05s var(--ease-out);
}
.js .case.reveal.is-in{ transform:none; }

/* ══════════════════════════════════════════════════════════════
   ACCESSIBILITY — three independent signals, each on its own terms.
   Reduced motion is not "no feedback"; it's gentler, non-vestibular
   feedback.
   ══════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion:reduce){
  /* keep the opacity change (it aids comprehension), drop the travel */
  .js .reveal{ transform:none; transition:opacity .28s var(--ease-out); }
  /* the case cards carry a higher-specificity rule above; it has to be
     answered here by name or the turn survives reduced motion */
  .js .case.reveal,
  .js .case.reveal.is-in{ transform:none; transition:opacity .28s var(--ease-out); }

  /* no drifting water, no swelling ridges, no scale-on-press */
  .footer__water .w1,.footer__water .w2{ animation:none; }

  .btn,.logo,.nav__links a,.nav__toggle,.footer__nav a{
    transition:background .2s linear, color .2s linear, border-color .2s linear;
  }
  .btn:active,.logo:active,.nav__links a:active,.nav__toggle:active,
  .footer__nav a:active{
    transform:none;
    opacity:.7;                     /* press feedback survives; motion doesn't */
  }
  .btn:hover svg{ transform:none; }
  .nav__toggle[aria-expanded="true"] span:first-child,
  .nav__toggle[aria-expanded="true"] span:last-child{ transition:none; }
}

@media (prefers-reduced-transparency:reduce){
  :root{ --material-bg:var(--paper); --material-edge:var(--line-2); --blur:0px; }
  .nav,.nav__links{
    background:var(--paper);
    backdrop-filter:none;
    -webkit-backdrop-filter:none;
  }
  .nav.is-stuck{ border-bottom-color:var(--line-2); }
  body::after{ display:none; }        /* the grain is a transparency effect too */
  .field{ opacity:.5; }
  .hero{ background:none; }           /* the sky wash is a transparency effect too */
  .range{ opacity:.5; }
}

@media (prefers-contrast:more){
  :root{
    --ink-2:#24292F;
    --ink-3:#3E444B;
    --line:#ADA79B;
    --line-2:#7E7869;
    --order:#1E4E60;
    --order-2:#123642;
    --chaos:#8F3616;
    --chaos-2:#6B2810;
    --material-bg:var(--paper);
    --blur:0px;
  }
  .nav{ background:var(--paper); backdrop-filter:none; -webkit-backdrop-filter:none; }
  .nav.is-stuck{ border-bottom-color:var(--ink); }
  .nav::after{ display:none; }
  .nav__links{ border-color:var(--ink); }
  .case{ border:1px solid var(--ink-3); }
  .btn--ghost{ border-color:var(--ink); }
  .wont{ border-style:solid; border-color:var(--chaos-2); }
  .faq details,.faq{ border-color:var(--ink-3); }
  body::after{ display:none; }
  .field{ opacity:.45; }
  .hero{ background:none; }           /* no wash behind text that must stay crisp */
  .range{ display:none; }
}

@media print{
  .nav,.field,.range,.btn,.footer__water{ display:none; }
  body{ background:#fff; color:#000; font-size:11pt; }
  body::after{ display:none; }
  .js .reveal{ opacity:1; transform:none; }
}
