/* ═══════════════════════════════════════════════════════════════════
   Ventra
   ═══════════════════════════════════════════════════════════════════

   Three values and one accent, and the accent is spent four times on the
   whole page: the V of the wordmark, the primary button, the ticks in the
   list of what you get, and the price. Deep green rather than deep blue,
   because a blue accent on a near-white page is the exact default this
   page is supposed to not look like.

   Separation is space, never a line. There is not one decorative border
   in this file. If two things need to be told apart, they get more room,
   and the only rules that exist are the ones under form fields, where a
   line is the control itself rather than decoration.
   ═══════════════════════════════════════════════════════════════════ */

:root{
  --ink:#0B0B0C;
  --paper:#FBFAF8;
  --muted:#6B6762;
  --accent:#16543F;
  --accent-ink:#FBFAF8;       /* text on the accent */
  --hair:rgba(11,11,12,.14);  /* form underlines only */
  --sink:rgba(11,11,12,.05);  /* the one tinted surface: the phone bezel */
  --lift:0 30px 70px rgba(11,11,12,.13), 0 6px 18px rgba(11,11,12,.07);

  /* the showcase runs on its own palette: the page goes dark for the
     work and comes back to paper afterwards. Tokens rather than hard
     colours, so everything inside the stage flips with one declaration. */
  --stage:#0B0B0C; --stage-ink:#F2F0ED; --stage-muted:#8E8A85;
  --stage-sink:rgba(242,240,237,.07);

  --ease:cubic-bezier(.16,1,.3,1);
  --r:14px;
  --gutter:clamp(20px,5vw,40px);
  --pad:clamp(80px,13vw,190px);
  --sans:"Inter Tight",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,
         Helvetica,Arial,sans-serif;
}
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --ink:#F2F0ED;
    --paper:#0B0B0C;
    --muted:#908B85;
    /* #16543F is a hole on near-black, so the dark theme carries the same
       green lifted into a range that actually reads on it */
    --accent:#4BA383;
    --accent-ink:#06130E;
    --hair:rgba(242,240,237,.16);
    --sink:rgba(242,240,237,.06);
    --lift:0 30px 70px rgba(0,0,0,.6), 0 6px 18px rgba(0,0,0,.4);
  --stage:#16161B; --stage-ink:#F2F0ED; --stage-muted:#8E8A85;
  --stage-sink:rgba(242,240,237,.09);
  }
}
:root[data-theme="dark"]{
  --ink:#F2F0ED; --paper:#0B0B0C; --muted:#908B85;
  --accent:#4BA383; --accent-ink:#06130E;
  --hair:rgba(242,240,237,.16); --sink:rgba(242,240,237,.06);
  --lift:0 30px 70px rgba(0,0,0,.6), 0 6px 18px rgba(0,0,0,.4);
  --stage:#16161B; --stage-ink:#F2F0ED; --stage-muted:#8E8A85;
  --stage-sink:rgba(242,240,237,.09);
}

/* ── type ─────────────────────────────────────────────────────────── */
@font-face{
  font-family:"Inter Tight";font-style:normal;font-weight:400;font-display:swap;
  src:url("../fonts/intertight-400.woff2") format("woff2");
}
@font-face{
  font-family:"Inter Tight";font-style:normal;font-weight:600;font-display:swap;
  src:url("../fonts/intertight-600.woff2") format("woff2");
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
/* NEVER overflow-x:hidden on html. It kills position:sticky, and the whole
   showcase is a sticky container. Anything that threatens to widen the
   document is contained on the element that causes it. */
body{
  margin:0;background:var(--paper);color:var(--ink);
  font-family:var(--sans);font-size:1.0625rem;line-height:1.6;
  font-weight:400;letter-spacing:-.006em;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
}
h1,h2,h3,p,figure,dl,dd{margin:0}
ul,ol{margin:0;padding:0;list-style:none}
img,svg{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
::selection{background:var(--accent);color:var(--accent-ink)}

.wrap{width:min(100%,1180px);margin-inline:auto;padding-inline:var(--gutter)}
.narrow{width:min(100%,760px)}

.h1{font-size:clamp(2.6rem,7vw,5.5rem);line-height:1.02;letter-spacing:-.03em;
  font-weight:600;text-wrap:balance}
.h2{font-size:clamp(1.9rem,3.8vw,3rem);line-height:1.08;letter-spacing:-.025em;
  font-weight:600;text-wrap:balance}
.lede{font-size:clamp(1.0625rem,1.6vw,1.25rem);line-height:1.55;max-width:34ch}
.muted{color:var(--muted)}
.small{font-size:.9375rem;line-height:1.5}

/* ── header ───────────────────────────────────────────────────────── */
/* No backdrop-filter anywhere on this bar. A blurred sticky header becomes
   a containing block for its fixed children, which breaks any overlay
   inside it the moment the page scrolls. There is no nav and no menu here,
   so the bar is a solid colour and the problem never arises. */
.top{position:sticky;top:0;z-index:50;background:var(--paper);
  transition:background .35s var(--ease),color .35s var(--ease)}
/* While the dark stage is under it the bar joins the stage, so the section
   reads full bleed instead of having a pale strip laid across the top of it.
   Same token remap, and the same explicit colour: see the note on the stage. */
.top.on-stage{
  --ink:var(--stage-ink); --muted:var(--stage-muted); --paper:var(--stage);
  background:var(--stage);color:var(--ink);
}
.top__in{display:flex;align-items:center;justify-content:space-between;
  gap:20px;padding-block:22px;
  /* viewport-fit=cover lets the page run under the status bar, so the bar
     pays for the notch itself rather than sitting beneath it */
  padding-top:calc(22px + env(safe-area-inset-top, 0px))}
.mark{display:block;width:clamp(104px,11vw,132px);color:var(--ink)}
.wm__l{fill:currentColor;stroke:none}
/* The drawn state is added by script. The default above is the finished
   artwork, so no-JS and reduced-motion both get a wordmark, not a blank. */
.wm--draw .wm__l{
  fill:transparent;stroke:currentColor;stroke-width:2.4;
  stroke-dasharray:var(--L);stroke-dashoffset:var(--L);
  animation:wmDraw 700ms var(--ease) forwards;
  animation-delay:calc(var(--i) * 55ms);
}
.wm--draw .wm__l:nth-child(1){color:var(--accent)}   /* accent use 1 of 4 */
.mark .wm__l:nth-child(1){fill:var(--accent)}
@keyframes wmDraw{
  0%  {stroke-dashoffset:var(--L);fill:transparent;stroke-opacity:1}
  65% {fill:transparent}
  100%{stroke-dashoffset:0;fill:currentColor;stroke-opacity:0}
}

.lang{display:flex;align-items:center;gap:2px;font-size:.9375rem;
  letter-spacing:.01em}
.lang button{
  appearance:none;background:none;border:0;padding:4px 7px;cursor:pointer;
  font:inherit;color:var(--muted);transition:color .18s var(--ease);
}
.lang button[aria-pressed="true"]{color:var(--ink)}
.lang button:hover{color:var(--ink)}
.lang span{color:var(--muted);opacity:.5}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}

/* the cross fade on switch: text only, so nothing moves */
.swap{transition:opacity .18s linear}
body.is-swapping .swap{opacity:0}

/* ── sections ─────────────────────────────────────────────────────── */
.sec{padding-block:var(--pad)}
.sec--tight{padding-block:calc(var(--pad) * .62)}

/* ── hero ─────────────────────────────────────────────────────────── */
.hero{padding-block:clamp(48px,9vw,120px) var(--pad)}
.hero__h{max-width:16ch}
.line{display:block;overflow:hidden}
.line > span{display:block}
.js .line > span{transform:translateY(105%);
  transition:transform .8s var(--ease);transition-delay:calc(var(--i) * 70ms)}
.js .hero.is-in .line > span{transform:none}
.hero__sub{margin-top:clamp(22px,3vw,30px)}
.hero__cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:clamp(30px,4vw,44px)}
.hero__note{margin-top:18px}

/* buttons: the fill comes in from the left, nothing scales or bounces */
.btn{
  position:relative;overflow:hidden;isolation:isolate;
  display:inline-flex;align-items:center;justify-content:center;
  padding:15px 28px;border-radius:999px;border:0;cursor:pointer;
  font:inherit;font-weight:600;font-size:1rem;letter-spacing:-.01em;
  background:transparent;color:var(--ink);
  box-shadow:inset 0 0 0 1.5px var(--ink);
  transition:color .26s var(--ease);
}
.btn::before{
  content:"";position:absolute;inset:0;z-index:-1;background:var(--ink);
  transform:scaleX(0);transform-origin:left center;
  transition:transform .26s var(--ease);
}
.btn:hover{color:var(--paper)}
.btn:hover::before{transform:scaleX(1)}
.btn--solid{                                    /* accent use 2 of 4 */
  background:var(--accent);color:var(--accent-ink);box-shadow:none;
}
.btn--solid::before{background:var(--ink)}
.btn--solid:hover{color:var(--paper)}

/* ── the showcase ─────────────────────────────────────────────────── */
/* Default state, which is also the no-JS and reduced-motion state: five
   entries stacked down the page, each with its phone and its line. Script
   adds .is-pinned, which lifts them on top of each other inside a sticky
   stage and deals them out on scroll. Same markup both ways. */
.work__intro{margin-bottom:clamp(40px,6vw,72px)}
.showcase{position:relative}
.showcase__in{display:grid;gap:clamp(48px,7vw,90px)}

/* The pair is centred as one object rather than pinned to the left
   edge of the wrap: a phone on the left with a 30ch caption beside it
   left a third of a 1440 screen as dead space, which reads as a layout
   that ran out of content rather than one that chose the space. */
.shot{display:grid;grid-template-columns:auto minmax(0,auto);
  gap:clamp(28px,5vw,76px);align-items:center;justify-content:center}
.cap__name{font-size:clamp(1.5rem,2.6vw,2.1rem);font-weight:600;
  letter-spacing:-.022em;line-height:1.1}
.cap__meta{margin-top:8px;font-size:.9375rem;letter-spacing:.02em;
  color:var(--muted)}
.cap__line{margin-top:18px;font-size:clamp(1.0625rem,1.5vw,1.2rem);
  line-height:1.5;max-width:30ch}

/* the phone. Shadow lives here and nowhere else on the page. */
.phone{
  position:relative;flex:none;width:clamp(236px,27vw,320px);
  aspect-ratio:290 / 620;border-radius:40px;padding:11px;
  background:var(--sink);box-shadow:var(--lift);
}
.phone__screen{
  position:relative;overflow:hidden;height:100%;border-radius:30px;
  background:var(--paper);
}
.phone__screen img{
  position:absolute;top:0;left:0;width:100%;height:auto;
  will-change:transform;
}

/* pinned mode */
.showcase.is-pinned{
  --ink:var(--stage-ink); --muted:var(--stage-muted);
  --paper:var(--stage); --sink:var(--stage-sink);
  background:var(--stage);
  /* RE-DECLARE colour here, do not rely on the token swap alone. `body`
     resolved `color: var(--ink)` once, against the page's --ink, and what
     descendants inherit is that COMPUTED colour, not the variable. Remapping
     --ink inside the stage therefore changes everything that reads the token
     in its own rule — the muted meta line did flip — while anything simply
     inheriting from body stayed near-black and vanished against the dark
     stage. The client names were invisible for exactly this reason. */
  color:var(--ink);
}
.showcase.is-pinned .showcase__in{display:block;position:sticky;
  height:100svh;top:0}
.showcase.is-pinned .shot{
  position:absolute;top:50%;left:0;right:0;
  transform:translateY(calc(-50% + var(--py,0px)));
  opacity:0;visibility:hidden;
  transition:opacity .38s var(--ease),visibility .38s;
}
.showcase.is-pinned .shot.is-on{opacity:1;visibility:visible}
/* the device settles rather than appearing at full size */
.showcase.is-pinned .shot .phone{transform:scale(.975);
  transition:transform .6s var(--ease)}
.showcase.is-pinned .shot.is-on .phone{transform:none}

.cap__name,.cap__meta,.cap__line{overflow:hidden}
.showcase.is-pinned .cap > * > span,
.showcase.is-pinned .cap__name > span{display:block;transform:translateY(105%);
  transition:transform .62s var(--ease)}
.showcase.is-pinned .shot.is-on .cap > * > span,
.showcase.is-pinned .shot.is-on .cap__name > span{transform:none}
.showcase.is-pinned .shot.is-on .cap__meta > span{transition-delay:70ms}
.showcase.is-pinned .shot.is-on .cap__line > span{transition-delay:140ms}

/* the rail */
.rail{display:none}
.showcase.is-pinned .rail{
  display:grid;gap:9px;position:absolute;left:4px;top:50%;
  transform:translateY(-50%);
}
.rail i{display:block;width:2px;height:18px;border-radius:2px;
  background:var(--ink);opacity:.16;
  transition:opacity .4s var(--ease),height .4s var(--ease)}
.rail i.is-on{height:26px}
.rail i.is-on{opacity:.85}
/* the phone itself does not fade between clients, only the strip inside it
   and the words beside it, so the device reads as one object being scrolled */
.showcase.is-pinned .shot .phone{opacity:1}
/* ── steps ────────────────────────────────────────────────────────── */
.steps{display:grid;gap:clamp(40px,5vw,64px);
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  margin-top:clamp(48px,6vw,80px)}
.step__n{font-size:.9375rem;color:var(--muted);letter-spacing:.04em;
  font-variant-numeric:tabular-nums}
.step h3{margin-top:14px;font-size:clamp(1.3rem,2vw,1.6rem);font-weight:600;
  letter-spacing:-.02em;line-height:1.15}
.step p{margin-top:12px;color:var(--muted);max-width:32ch}

/* ── what you get ─────────────────────────────────────────────────── */
.gets{display:grid;gap:18px 56px;grid-template-columns:repeat(2,minmax(0,1fr));
  margin-top:clamp(44px,6vw,72px)}
.gets li{display:grid;grid-template-columns:18px minmax(0,1fr);gap:14px;
  align-items:start}
.gets svg{width:18px;height:18px;margin-top:6px;color:var(--accent)} /* 3 of 4 */

/* ── price ────────────────────────────────────────────────────────── */
.price{display:grid;justify-items:start;gap:clamp(20px,3vw,30px)}
.price__big{
  font-size:clamp(4rem,13vw,10rem);line-height:.92;letter-spacing:-.045em;
  font-weight:600;color:var(--accent);                 /* accent use 4 of 4 */
  font-variant-numeric:tabular-nums;
}
.price__then{font-size:clamp(1.2rem,2.4vw,1.75rem);letter-spacing:-.02em;
  line-height:1.2}
.price__note{max-width:46ch;color:var(--muted)}

/* ── contact ──────────────────────────────────────────────────────── */
.contact{display:grid;gap:clamp(44px,6vw,86px);
  grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);align-items:start}
.reach{display:grid;gap:14px;margin-top:30px}
.reach a,.reach span{font-size:1.0625rem}
.reach a{box-shadow:inset 0 -1px 0 var(--hair);transition:box-shadow .2s}
.reach a:hover{box-shadow:inset 0 -1px 0 var(--ink)}

.form{display:grid;gap:26px;justify-items:stretch}
.form button[type=submit]{justify-self:start;padding-inline:40px}
.field{display:grid;gap:7px}
.field label{font-size:.875rem;letter-spacing:.02em;color:var(--muted)}
.field input,.field select,.field textarea{
  font:inherit;color:var(--ink);background:transparent;
  border:0;border-bottom:1px solid var(--hair);border-radius:0;
  padding:9px 0;appearance:none;transition:border-color .2s;
}
.field textarea{resize:vertical;min-height:92px}
.field select{cursor:pointer}
.field input:focus,.field select:focus,.field textarea:focus{
  outline:none;border-bottom-color:var(--accent)}
.hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0}
.formmsg{min-height:1.4em;font-size:.9375rem;color:var(--accent)}
.formmsg[data-state="err"]{color:#C2453C}
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]) .formmsg[data-state="err"]{color:#F08A82}
}

/* ── footer ───────────────────────────────────────────────────────── */
.foot{padding-block:48px 56px;color:var(--muted);font-size:.9375rem}
.foot__in{display:flex;flex-wrap:wrap;gap:12px 28px;align-items:center}
.foot a:hover{color:var(--ink)}
.foot .lang{margin-left:auto}

/* ── reveals ──────────────────────────────────────────────────────── */
/* The finished state is the default. Motion is added only for a browser
   that can actually do it, because a browser that drops one line of an
   animation shorthand and keeps `both` fires every section at once, in the
   wrong place, with no way back. */
.rv{opacity:1;transform:none}
@supports (animation-timeline: scroll()) or (transition: opacity 1ms){
  .js .rv{opacity:0;transform:translateY(18px);
    transition:opacity .5s var(--ease),transform .5s var(--ease);
    transition-delay:var(--d,0ms)}
  .js .rv.is-in{opacity:1;transform:none}
}

/* ── responsive ───────────────────────────────────────────────────── */
@media (max-width:900px){
  .shot{grid-template-columns:minmax(0,1fr);justify-items:center;
    text-align:center;gap:30px}
  .cap__line{margin-inline:auto}
  .contact{grid-template-columns:minmax(0,1fr)}
  /* Horizontal reveals widen the document on a phone, which loads the page
     zoomed out with the header off screen. Vertical only down here. */
  .js .rv{transform:translateY(14px)}
}

/* ── the showcase on a phone ──────────────────────────────────────
   NO DRAWN PHONE ON A PHONE. The desktop version puts the client's
   site inside a device mockup, which is the right idea on a laptop and
   an absurd one here: it is a picture of a phone, displayed on a phone,
   at a third of the size, so the work it exists to show ends up a
   thumbnail floating in a field of black.

   Below 720px the frame is simply deleted. The visitor is holding the
   device, so the client's site runs edge to edge and full height, and
   their own screen is the bezel. The caption moves off the floor and
   onto the image on a scrim, and the progress moves to segments across
   the top, where a phone expects to find them.

   Everything here is layout. The scroll maths, the strips and the
   captions are the same objects as on desktop. */
@media (max-width:720px){
  .showcase.is-pinned .showcase__in{
    /* out of the wrap's gutter: this section is the one full-bleed
       thing on the page */
    width:100%;max-width:none;padding-inline:0;margin-inline:0;
  }
  .showcase.is-pinned .shot{
    position:absolute;inset:0;top:0;transform:none;display:block;
    padding:0;gap:0;
  }
  .showcase.is-pinned .shot .phone{
    position:absolute;inset:0;width:auto;aspect-ratio:auto;
    padding:0;border-radius:0;background:none;box-shadow:none;transform:none;
  }
  .showcase.is-pinned .shot.is-on .phone{transform:none}
  .showcase.is-pinned .phone__screen{border-radius:0;background:var(--stage)}

  /* the caption rides on the image, bottom left, over a scrim */
  .showcase.is-pinned .cap{
    position:absolute;left:0;right:0;bottom:0;z-index:2;
    text-align:left;padding:84px 22px calc(26px + env(safe-area-inset-bottom,0px));
    background:linear-gradient(180deg,transparent,rgba(8,8,10,.62) 48%,rgba(8,8,10,.95));
    color:#F2F0ED;
  }
  .showcase.is-pinned .cap__name{font-size:2rem;letter-spacing:-.025em}
  .showcase.is-pinned .cap__meta{color:#B3ADA6}
  .showcase.is-pinned .cap__line{font-size:1.0625rem;max-width:30ch;
    margin-inline:0;margin-top:12px}

  /* progress as segments across the top, the way a phone expects them */
  .showcase.is-pinned .rail{
    display:flex;gap:5px;position:absolute;z-index:3;
    left:14px;right:14px;width:auto;transform:none;
    /* --topH is the bar's measured height, written by the script. A fixed
       guess put the segments underneath the bar, where they were invisible,
       and the bar's height moves with the notch and the type size. */
    top:calc(var(--topH, 68px) + 12px);
  }
  .showcase.is-pinned .rail i{
    flex:1 1 0;width:auto;height:2.5px;border-radius:2px;
    background:#F2F0ED;opacity:.28;transition:opacity .35s var(--ease);
  }
  /* height is reset here on purpose: the desktop rule grows the active
     tick to 26px, which on a 2.5px horizontal bar drew a white square */
  .showcase.is-pinned .rail i.is-on{height:2.5px;opacity:1}
}
@media (max-width:560px){
  .gets{grid-template-columns:minmax(0,1fr);gap:16px}
  .btn{width:100%}
  .form button[type=submit]{justify-self:stretch}
  .hero__cta{gap:10px}
}

/* ── reduced motion ───────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms!important;animation-iteration-count:1!important;
    transition-duration:.01ms!important;scroll-behavior:auto!important}
  .js .rv{opacity:1;transform:none}
}
