/* ============================================================
   arbitas.lt  ·  v4
   Direction: the page is lit by the work.

   v4 changes one thing structurally and it is the whole point:
   the light is no longer three stacked lamps fading between fixed
   positions. It is ONE continuous source whose position, hue and
   intensity live in custom properties that a single rAF loop lerps
   every frame. Nothing about the light steps, ever.

   The glass drinks from the same properties, so the panels tint with
   the room instead of sitting on top of it as grey plastic.
   ============================================================ */

:root{
  /* base - neutral on purpose, so all chroma is content-derived */
  --void:#08090c;
  --slate:#11131a;
  --porcelain:#f2f0ec;
  --ash:#8a8f99;
  --edge:rgba(242,240,236,.13);

  /* ---- the light. app.js writes these; CSS only ever reads them. ---- */
  --lx:78%;          /* beam centre x */
  --ly:16%;          /* beam centre y */
  --lc:201 90 46;    /* beam colour, space-separated for modern rgb() slash syntax */
  --li:0;            /* intensity 0-1, eased in after boot */

  /* Display serif carries the voice, the grotesk carries the reading, the mono
     carries anything that is literally an address or a spec. Three roles, three
     faces, no overlap - and none of them is whatever font the OS felt like. */
  --serif:'DM Serif Display',Georgia,'Times New Roman',serif;
  --display:'Instrument Sans',system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --mono:ui-monospace,"Cascadia Mono","SF Mono",Consolas,"Liberation Mono",monospace;

  /* One signature curve carries ~80% of the page and stays exactly as it
     was. What was missing was its opposite number: entrances decelerate,
     exits do the reverse. Using the entrance curve in both directions is
     what made every hover leave as slowly as it arrived. */
  --ease:cubic-bezier(.22,1,.36,1);
  --ease-exit:cubic-bezier(.3,0,1,1);
  /* A fade is not a spatial move. A hard decelerate front-loads a pure
     opacity change into its first third, so the swap reads as a snap and
     then a long invisible tail. Fades want symmetry. */
  --ease-fade:cubic-bezier(.37,0,.63,1);
  --gut:clamp(20px,4vw,34px);
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--void);color:var(--porcelain);
  font-family:var(--display);font-size:17px;line-height:1.6;
  -webkit-font-smoothing:antialiased;overflow-x:hidden;
}
img{max-width:100%;height:auto;display:block}
a{color:inherit;text-decoration:none}
:focus-visible{outline:2px solid var(--porcelain);outline-offset:3px;border-radius:4px}

.shell{width:100%;max-width:1200px;margin:0 auto;padding:0 var(--gut)}

/* ---------- the light rig ----------
   Two beams: a wide soft wash that carries the room, and a tighter core
   that gives the source a believable centre. Both read the same four
   properties, so they can never disagree or fall out of sync. */
/* inset:0 is deliberate. With a negative inset the beam box is larger than the
   viewport, so --lx/--ly would address that larger box and the light would sit
   somewhere other than where the pointer and scroll think it is. Softness comes
   from the gradient falloff and the blur, never from oversizing the box. */
.rig{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;contain:strict}
/* Dispersed, not directed. The brief is light you notice only when it moves:
   a room that is very slightly warmer on one side, not a lamp pointed at the
   page. Low alphas, enormous radii and heavy blur do that; the responsiveness
   lives entirely in the loop, so making the light fainter costs it nothing. */
.beam{position:absolute;inset:0;opacity:var(--li);will-change:opacity}
.beam-wide{
  background:radial-gradient(circle 130vmax at var(--lx) var(--ly),
    rgb(var(--lc) / .30) 0%,
    rgb(var(--lc) / .17) 22%,
    rgb(var(--lc) / .08) 44%,
    rgb(var(--lc) / .03) 66%,
    transparent 88%);
  /* 40px, not 120px. The gradient is already enormously soft - its stops sit at
     0/22/44/66/88% of a 130vmax circle - so a huge blur radius on top of it adds
     almost nothing visible while costing a great deal of paint. Measured with
     the light frozen so only the radius differed: no pixel changes by more than
     14/255 and the mean change is 0.66/255, which is invisible. Measured on a
     4x-throttled CPU it removes the worst scroll frames (spikes of 100-117ms
     are gone; the worst frame is now a consistent 83ms).
     Do not raise it back without re-measuring both. */
  filter:blur(40px);
}
.beam-core{
  background:radial-gradient(circle 62vmax at var(--lx) var(--ly),
    rgb(var(--lc) / .22) 0%,
    rgb(var(--lc) / .09) 34%,
    transparent 72%);
  filter:blur(50px);   /* was 150px, same reasoning as .beam-wide above */
  mix-blend-mode:screen;
}
.grain{
  position:absolute;inset:-50%;opacity:.17;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
main,header,footer{position:relative;z-index:1}

/* ---------- nav ---------- */
.top{position:sticky;top:0;z-index:60;border-bottom:1px solid transparent;
  transition:background .26s var(--ease-exit),border-color .26s var(--ease-exit)}
.top.stuck{transition:background .26s var(--ease),border-color .26s var(--ease)}
.top.stuck{
  background:rgba(8,9,12,.62);border-bottom-color:var(--edge);
  -webkit-backdrop-filter:blur(18px) saturate(150%);backdrop-filter:blur(18px) saturate(150%);
}
.top-in{display:flex;align-items:center;justify-content:space-between;height:70px;gap:16px}
.mark{font-family:var(--serif);font-size:1.42rem;font-weight:400;letter-spacing:-.005em}
.mark i{font-style:normal;opacity:.42}

/* ---------- buttons ---------- */
.act{
  --b:var(--porcelain);--f:var(--void);
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  background:var(--b);color:var(--f);font-weight:640;font-size:.98rem;
  padding:14px 26px;border-radius:999px;border:1px solid transparent;
  position:relative;overflow:hidden;isolation:isolate;
  /* The transition declared here governs LEAVING; the one on :hover governs
     ARRIVING. That is the whole mechanism for directional easing. */
  transition:transform .28s var(--ease-exit),box-shadow .28s var(--ease-exit);
}
.act::after{
  content:"";position:absolute;inset:0;z-index:-1;translate:-120% 0;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.55),transparent);
  transition:translate .65s var(--ease-exit);
}
/* Follow-through: the shadow runs slower than the lift, so it settles after
   the button has already stopped. That lag is the difference between a button
   that moved and a button that has weight. */
.act:hover{transform:translateY(-2px);box-shadow:0 14px 34px rgb(var(--lc) / .28),0 14px 34px rgba(0,0,0,.5);
  transition:transform .28s var(--ease),box-shadow .42s var(--ease)}
.act:hover::after{translate:120% 0;transition:translate .65s var(--ease)}
/* Press. The page had hover states and no press states, which on a phone -
   where hover does not exist - meant tapping the main CTA acknowledged
   nothing at all. Under 150ms by rule, and no overshoot: this palette does
   not bounce. */
.act:active{transform:scale(.985) translateY(0);transition-duration:.09s}
.act.hollow{
  background:rgb(var(--lc) / .07);color:var(--porcelain);border-color:var(--edge);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.act.hollow:hover{border-color:rgb(var(--lc) / .5)}
.act.small{padding:11px 20px;font-size:.88rem}
.acts{display:flex;flex-wrap:wrap;gap:12px;margin-top:34px}

/* ---------- type ---------- */
.eyebrow{
  font-family:var(--mono);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;
  color:var(--ash);display:block;margin-bottom:20px;
}
/* Lithuanian sets lower than most Latin text: ą ę į ų carry ogoneks BELOW the
   baseline and ū ė š ž carry marks ABOVE it, so a line box tuned for English
   clips at both ends. Line height stays generous enough to contain them. */
/* DM Serif Display ships one weight, 400, and that is the point: its authority
   comes from the drawing, not from being shouted in bold. It also wants far
   less negative tracking than a grotesk - tightening a high-contrast serif
   collides the thin strokes. */
h1,h2,h3{font-family:var(--serif);font-weight:400;letter-spacing:0;line-height:1.1;margin:0}
/* The hero sets tighter than the rest. It is four short lines read as one
   block, so it wants to look like a paragraph of display type rather than four
   separate statements. Safe to tighten because the clipping window's slack
   comes from .kin .ln's padding, not from the line height. */
h1{font-size:clamp(2.9rem,6.6vw,5.2rem);line-height:.98}
h2{font-size:clamp(2rem,4vw,3.1rem)}
h3{font-size:clamp(1.45rem,2.2vw,1.95rem);line-height:1.16}
.lede{font-size:clamp(1.04rem,1.7vw,1.26rem);color:var(--ash);max-width:54ch;margin:22px 0 0;line-height:1.55}
p{margin:0 0 16px}

/* ---------- kinetic headings ----------
   Each line is a clipping window; the inner <i> is what actually moves.
   No JS is required for the text to be readable - the hidden state is
   gated on .js exactly like every other reveal in this file. */
/* The clipping window needs slack at BOTH ends or it shaves the language:
   ogoneks hang below the baseline, diacritics sit above the cap height. The
   padding opens the box, the equal negative margin takes the space back out of
   the layout, so nothing shifts visually and nothing gets cut. */
.kin .ln{display:block;overflow:hidden;padding:.16em 0 .26em;margin:-.16em 0 -.26em}
.kin .ln i{display:block;font-style:inherit}
.hero h1 .dim{color:var(--ash)}
.js .kin .ln i{transform:translateY(115%);transition:transform 1s var(--ease)}
.js .kin.up .ln i{transform:none}
.js .kin.up .ln:nth-child(1) i{transition-delay:.02s}
.js .kin.up .ln:nth-child(2) i{transition-delay:.1s}
.js .kin.up .ln:nth-child(3) i{transition-delay:.18s}
.js .kin.up .ln:nth-child(4) i{transition-delay:.26s}
/* The hero headline is the action beat of the opening sequence below, so its
   lines start fractionally later than a scroll-triggered heading's would -
   after the eyebrow has placed the room, not on top of it. */
.js .hero h1.kin .ln:nth-child(1) i{transition-delay:.10s}
.js .hero h1.kin .ln:nth-child(2) i{transition-delay:.18s}
.js .hero h1.kin .ln:nth-child(3) i{transition-delay:.26s}
.js .hero h1.kin .ln:nth-child(4) i{transition-delay:.34s}

/* ---------- sections ---------- */
.band{padding:clamp(74px,6.5vw,112px) 0}
/* Measure is constrained, alignment is not. When .band-hd lands on the same
   element as .shell its max-width would otherwise beat the shell's and the
   auto margins would centre the whole header, leaving section intros floating
   in from the left edge while every other block sits on the gutter. */
.band-hd{max-width:60ch}
.shell.band-hd{max-width:1200px}
.shell.band-hd>*{max-width:60ch}

/* ---------- hero ---------- */
.hero{padding:clamp(44px,4vw,80px) 0 clamp(60px,5.5vw,96px)}
.hero-grid{display:grid;gap:clamp(48px,7vw,72px);align-items:center}
@media(min-width:1000px){.hero-grid{grid-template-columns:1.04fr .96fr}}

/* the device: the single memorable object.
   --rx/--ry come from the pointer, --p from scroll. One transform, so the
   two inputs compose instead of overwriting each other. */
.rack{position:relative;perspective:1400px}
.pane{
  --rx:0deg;--ry:0deg;--p:0;
  position:relative;border-radius:14px;overflow:hidden;
  background:var(--slate);border:1px solid var(--edge);
  box-shadow:0 40px 90px rgba(0,0,0,.62),
             0 0 0 1px rgba(255,255,255,.03) inset,
             0 24px 90px rgb(var(--lc) / .18);
  transform-style:preserve-3d;
  transform:rotateY(var(--ry)) rotateX(var(--rx)) translate3d(0,calc(var(--p) * -18px),0);
  will-change:transform;
}
.chrome{
  display:flex;align-items:center;gap:7px;padding:11px 14px;
  background:rgba(242,240,236,.045);border-bottom:1px solid var(--edge);
}
.chrome i{width:9px;height:9px;border-radius:50%;background:rgba(242,240,236,.2)}
.chrome b{
  font-family:var(--mono);font-weight:400;font-size:.72rem;color:var(--ash);
  margin-left:10px;padding:4px 12px;border-radius:99px;background:rgba(8,9,12,.55);
}
.shots{position:relative;aspect-ratio:16/10;background:var(--void)}
/* Layered swap, not a crossfade. Two dense screenshots at 50% each read as
   mush, so the incoming shot fades in ON TOP of the outgoing one, which stays
   fully opaque underneath until it is completely covered. */
/* display:contents removes <picture> from layout entirely, so the <img> stays a
   direct child for positioning purposes and the art-direction swap costs the
   layout nothing. Without it the absolutely-positioned shots would have an
   inline box sitting between them and .shots. */
.shots picture{display:contents}
.shots img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top center;
  /* Duration stays at .7s deliberately: app.js holds the outgoing image
     underneath for 760ms, so a longer fade would expose the void mid-swap.
     The curve was the thing that needed changing, not the time. */
  opacity:0;z-index:1;transition:opacity .7s var(--ease-fade);
}
.shots img.on{opacity:1;z-index:3}
.shots img.under{opacity:1;z-index:2;transition:none}

/* domain switcher - mono, because these are addresses, not menu items */
.dials{display:flex;flex-wrap:wrap;gap:8px;margin-top:22px;padding:0;list-style:none}
.dial{
  font-family:var(--mono);font-size:.76rem;letter-spacing:.01em;
  padding:9px 15px;border-radius:999px;cursor:pointer;
  background:rgb(var(--lc) / .07);border:1px solid var(--edge);color:var(--ash);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  transition:color .26s var(--ease-exit),border-color .26s var(--ease-exit),
             background .26s var(--ease-exit),transform .2s var(--ease-exit);
}
.dial:hover{color:var(--porcelain);
  transition:color .26s var(--ease),border-color .26s var(--ease),background .26s var(--ease)}
.dial:active{transform:scale(.96);transition-duration:.09s}
.dial[aria-selected="true"]{color:var(--void);background:var(--porcelain);border-color:transparent}

/* ---------- work ---------- */
.case{padding:clamp(56px,5vw,90px) 0}
.case-grid{display:grid;gap:clamp(34px,5vw,64px);align-items:center}
@media(min-width:1000px){
  .case-grid{grid-template-columns:1.06fr .94fr}
  .case.flip .case-shot{order:2}
}
.case-shot .pane{box-shadow:0 34px 74px rgba(0,0,0,.55),0 20px 70px rgb(var(--lc) / .2)}
/* The case shot flows at its own aspect rather than being forced into the
   hero's 16/10 box, because on phones it swaps to a real portrait screenshot. */
/* aspect-ratio:auto on the IMG is the load-bearing bit. The width/height
   attributes describe the desktop capture, so once <picture> swaps in the
   portrait phone capture the attribute-derived ratio would squash it into a
   landscape box and object-fit:cover would then crop the sides off. Letting the
   image use its own intrinsic ratio makes both captures render whole. */
.case-shot .shots{aspect-ratio:auto}
.case-shot .shots img{
  opacity:1;position:static;width:100%;height:auto;
  aspect-ratio:auto;object-fit:fill;
}
.tagline{font-family:var(--mono);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:var(--ash)}
.case-copy h3{margin:14px 0 16px}
.case-copy p{color:var(--ash);max-width:48ch}
.facts{display:flex;flex-wrap:wrap;gap:7px;margin:22px 0 26px;padding:0;list-style:none}
.facts li{
  font-family:var(--mono);font-size:.72rem;color:var(--ash);
  padding:6px 12px;border-radius:99px;border:1px solid var(--edge);
  background:rgb(var(--lc) / .05);
}
.open-link{
  font-family:var(--mono);font-size:.86rem;display:inline-flex;align-items:center;gap:8px;
  padding-bottom:4px;border-bottom:1px solid var(--edge);transition:border-color .26s var(--ease-exit)
}
.open-link:hover{border-bottom-color:var(--porcelain);transition:border-color .26s var(--ease)}
.open-link em{font-style:normal;transition:translate .26s var(--ease-exit)}
.open-link:hover em{translate:3px -3px;transition:translate .26s var(--ease)}

/* ---------- glass ----------
   The panels tint with the room. This is the whole reason the glass and the
   backlight belong to one system rather than two effects sitting side by side. */
.plans{display:grid;gap:20px;margin-top:52px}
@media(min-width:860px){.plans{grid-template-columns:1fr 1fr}}
.plan{
  background:
    linear-gradient(160deg, rgb(var(--lc) / .05), rgb(var(--lc) / .01) 55%),
    rgba(242,240,236,.045);
  border:1px solid var(--edge);border-radius:20px;
  padding:clamp(26px,3.4vw,38px);
  -webkit-backdrop-filter:blur(20px) saturate(140%);backdrop-filter:blur(20px) saturate(140%);
  position:relative;overflow:hidden;
}
.plan::before{
  content:"";position:absolute;inset:0 0 auto;height:1px;
  background:linear-gradient(90deg,transparent,rgb(var(--lc) / .75),transparent);
}
/* These two panels decide whether anyone calls, and they were the only large
   cards on the page with no response to a cursor at all. The tint is drawn
   from --lc, the light rig's live colour, so they warm toward whatever the
   room is currently lit by instead of a hardcoded accent. */
.plan{transition:transform .42s var(--ease-exit),box-shadow .52s var(--ease-exit),
                 border-color .42s var(--ease-exit)}
@media (hover:hover){
  .plan:hover{
    transform:translateY(-3px);
    border-color:rgb(var(--lc) / .42);
    box-shadow:0 22px 50px rgba(0,0,0,.45),0 10px 40px rgb(var(--lc) / .16);
    transition:transform .38s var(--ease),box-shadow .52s var(--ease),
               border-color .38s var(--ease);
  }
}
.plan.first{background:
    linear-gradient(160deg, rgb(var(--lc) / .07), rgb(var(--lc) / .015) 55%),
    rgba(242,240,236,.075)}
.tag{font-family:var(--mono);font-size:.7rem;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
.fee{font-family:var(--serif);font-size:clamp(2.3rem,4.2vw,3.1rem);font-weight:400;letter-spacing:-.01em;margin:14px 0 6px;line-height:1.04}
.fee small{font-family:var(--mono);font-size:.7rem;letter-spacing:.1em;color:var(--ash);font-weight:400}
.plan .who{color:var(--ash);font-size:.95rem;margin:0}
.plan ul{list-style:none;padding:0;margin:24px 0 0;display:grid;gap:12px}
.plan li{position:relative;padding-left:26px;font-size:.96rem;color:var(--porcelain)}
.plan li::before{
  content:"";position:absolute;left:0;top:.55em;width:12px;height:6px;
  border-left:1.8px solid var(--porcelain);border-bottom:1.8px solid var(--porcelain);
  transform:rotate(-45deg);opacity:.75;
}
/* The ticks are the specification of what the money buys, so they get written
   into the panel after it lands rather than arriving fully formed with it.
   Anchored at the corner they pivot around, so it reads as a stroke being
   drawn rather than a shape being scaled up. .plan carries .lift itself, so
   .up lands on the panel. */
.js .plan li::before{transform:rotate(-45deg) scale(0);transform-origin:0 100%;
  transition:transform .3s var(--ease)}
.js .plan.up li::before{transform:rotate(-45deg) scale(1)}
.js .plan li:nth-child(1)::before{transition-delay:.28s}
.js .plan li:nth-child(2)::before{transition-delay:.34s}
.js .plan li:nth-child(3)::before{transition-delay:.40s}
.js .plan li:nth-child(4)::before{transition-delay:.46s}
.js .plan li:nth-child(5)::before{transition-delay:.52s}
.js .plan li:nth-child(6)::before{transition-delay:.58s}
.footnote{margin-top:28px;color:var(--ash);font-size:.93rem;max-width:66ch}

/* ---------- process ---------- */
.flow{display:grid;gap:2px;margin-top:52px;border-top:1px solid var(--edge)}
@media(min-width:820px){.flow{grid-template-columns:repeat(4,1fr);gap:0}}
.beat{padding:26px 0 30px;border-bottom:1px solid var(--edge)}
@media(min-width:820px){
  .beat{border-bottom:none;padding:30px 26px 0 0}
  .beat + .beat{border-left:1px solid var(--edge);padding-left:26px}
}
.beat .n{font-family:var(--mono);font-size:.72rem;color:var(--ash);letter-spacing:.16em;display:block;margin-bottom:16px}
.beat h3{font-size:1.16rem;margin-bottom:10px}
.beat p{color:var(--ash);font-size:.94rem;margin:0}

/* ---------- faq ---------- */
.qa{margin-top:44px;max-width:820px;border-top:1px solid var(--edge)}
.qa details{border-bottom:1px solid var(--edge)}
.qa summary{
  cursor:pointer;list-style:none;padding:24px 44px 24px 0;position:relative;
  font-size:clamp(1.02rem,1.6vw,1.16rem);font-weight:600;letter-spacing:-.02em;
}
.qa summary::-webkit-details-marker{display:none}
.qa summary::after,.qa summary::before{
  content:"";position:absolute;right:10px;top:50%;background:var(--ash);
  transition:transform .34s var(--ease-exit),background .34s var(--ease-exit);
}
.qa details[open] summary::after,.qa details[open] summary::before{
  transition:transform .34s var(--ease),background .34s var(--ease);
}
.qa summary{transition:opacity .2s var(--ease-exit)}
.qa summary:active{opacity:.72;transition-duration:.08s}

/* The answers used to appear as a jump cut while the +/- icon animated
   politely beside content that had already teleported into place. This is
   pure progressive enhancement: browsers without ::details-content or
   interpolate-size get exactly the old snap, which is current behaviour
   rather than a broken one. No JS, no measuring, no height hacks. */
@supports (interpolate-size: allow-keywords){
  :root{interpolate-size:allow-keywords}
  .qa details::details-content{
    block-size:0;overflow:hidden;opacity:0;
    transition:block-size .34s var(--ease),
               opacity .24s var(--ease-exit),
               content-visibility .34s allow-discrete;
  }
  /* Opacity trails the height slightly, so the text arrives into a space
     that is already open instead of racing the box. */
  .qa details[open]::details-content{
    block-size:auto;opacity:1;
    transition:block-size .34s var(--ease),
               opacity .26s var(--ease) .08s,
               content-visibility .34s allow-discrete;
  }
}
.qa summary::after{width:13px;height:1.5px;margin-top:-.75px}
.qa summary::before{width:1.5px;height:13px;margin-top:-6.5px;right:15.75px}
.qa details[open] summary::before{transform:scaleY(0)}
.qa details[open] summary::after{background:var(--porcelain)}
.qa .ans{padding:0 0 26px;color:var(--ash);max-width:66ch}

/* ---------- close ---------- */
.close-panel{
  margin-top:0;text-align:center;border-radius:26px;padding:clamp(48px,7vw,86px) var(--gut);
  background:
    linear-gradient(165deg, rgb(var(--lc) / .06), rgb(var(--lc) / .012) 60%),
    rgba(242,240,236,.05);
  border:1px solid var(--edge);
  -webkit-backdrop-filter:blur(22px) saturate(150%);backdrop-filter:blur(22px) saturate(150%);
  position:relative;overflow:hidden;
}
.close-panel::before{
  content:"";position:absolute;inset:0 0 auto;height:1px;
  background:linear-gradient(90deg,transparent,rgb(var(--lc) / .8),transparent);
}
.close-panel p{color:var(--ash);max-width:48ch;margin:22px auto 0}
.close-panel .acts{justify-content:center}
.wire{
  display:inline-block;margin-top:30px;font-family:var(--mono);font-size:.92rem;color:var(--ash);
  border-bottom:1px solid var(--edge);padding-bottom:3px;
  transition:color .26s var(--ease-exit),border-color .26s var(--ease-exit);
}
.wire:hover{color:var(--porcelain);border-bottom-color:var(--porcelain);
  transition:color .26s var(--ease),border-color .26s var(--ease)}

/* ---------- footer ---------- */
.foot{padding:44px 0 54px;border-top:1px solid var(--edge);margin-top:clamp(72px,6vw,112px)}
.foot-in{display:flex;flex-wrap:wrap;gap:14px;justify-content:space-between;align-items:center;
  font-family:var(--mono);font-size:.76rem;color:var(--ash)}
.foot a{transition:color .26s var(--ease-exit)}
.foot a:hover{color:var(--porcelain);transition:color .26s var(--ease)}

/* ---------- motion system ----------
   Hidden states are scoped to .js, which app.js sets on itself as its first
   statement. If the script 404s or throws, the class is never added and every
   section renders normally. Content must never depend on JS to be visible. */
/* Duration scales with distance: 100px is the reference. .lift travels 26px
   and .stagger 20px, so at .85s and .8s both were running roughly twice as
   long as the trip they describe. That is the textbook "feels too slow".

   Shortening them also fixed a second thing for free. At .8s with 80ms steps,
   all six staggered children were in flight simultaneously for the entire
   sequence. At .42s child one lands as child six starts, so the sequence
   self-limits to about three moving at once. The delays below are therefore
   unchanged: 440ms total, still inside the 500ms stagger budget. */
.js .lift{opacity:0;transform:translateY(26px);transition:opacity .48s var(--ease),transform .48s var(--ease)}
.js .lift.up{opacity:1;transform:none}
.js .stagger>*{opacity:0;transform:translateY(20px);transition:opacity .42s var(--ease),transform .42s var(--ease)}
.js .stagger.up>*{opacity:1;transform:none}
.js .stagger.up>*:nth-child(1){transition-delay:.04s}
.js .stagger.up>*:nth-child(2){transition-delay:.12s}
.js .stagger.up>*:nth-child(3){transition-delay:.2s}
.js .stagger.up>*:nth-child(4){transition-delay:.28s}
.js .stagger.up>*:nth-child(5){transition-delay:.36s}
.js .stagger.up>*:nth-child(6){transition-delay:.44s}

/* ---------- the double entrance, removed ----------
   Every section header is <div class="band-hd lift"> wrapping an
   <h2 class="kin">. So the block rose 26px while, inside it, the heading's
   clipping windows slid their type up 115%. Two entrance animations on one
   object, at one moment, on different durations.

   That is interference, not richness. A kinetic heading works by holding a
   window perfectly still and moving type through it; if the window is also
   travelling, the effect it exists to produce is the one thing being smeared.
   The most distinctive motion on this site was being undercut by a generic
   reveal wrapped around it.

   The wrapper keeps .lift because that is the observer's hook for .up, but it
   no longer moves. The heading owns the section entrance, which is what it
   was written to do. The eyebrow keeps a small rise of its own so the block
   still has a lead-in rather than simply appearing. */
.js .band-hd.lift{opacity:1;transform:none;transition:none}
.js .band-hd.lift .eyebrow{opacity:0;transform:translateY(14px);
  transition:opacity .42s var(--ease),transform .42s var(--ease)}
.js .band-hd.lift.up .eyebrow{opacity:1;transform:none}

/* Lead with the hero, then let it land. The process grid and the heading
   above it are observed independently, so on a fast scroll both crossed the
   trigger line together and the section arrived as one undifferentiated wave.
   160ms is enough for the eye to register that the heading came first.
   Scoped to .flow only: the case studies' .stagger blocks contain their own
   headings and are already correctly ordered. */
.js .flow.stagger.up>*:nth-child(1){transition-delay:.16s}
.js .flow.stagger.up>*:nth-child(2){transition-delay:.24s}
.js .flow.stagger.up>*:nth-child(3){transition-delay:.32s}
.js .flow.stagger.up>*:nth-child(4){transition-delay:.40s}

/* opening sequence */
@keyframes rise{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:none}}
@keyframes settle{from{opacity:0;transform:translateY(46px) scale(.965)}to{opacity:1;transform:none}}
/* Setup, action, resolution. Before, the nav, eyebrow, headline, lede,
   buttons and device were all in motion between .06s and 1.45s, overlapping
   throughout: everything moved, so nothing led.

     0.00-0.60  the eyebrow places the room          setup
     0.10-1.34  the headline writes itself           action
     0.45-1.45  the device settles in behind it      resolution
     0.60-1.32  the lede and the buttons arrive      resolution

   The device's settle drops from 1.25s to 1s. It has not lost its weight, it
   has stopped being the reason the opening runs long. Everything still ends
   comfortably before app.js removes .boot at 2200ms. */
.boot .top-in,.boot .hero-copy>.eyebrow,.boot .hero-copy>.lede,.boot .hero-copy>.acts{
  animation:rise .6s var(--ease) both}
.boot .hero-copy>.eyebrow{animation-delay:0s}
.boot .hero-copy>.lede{animation-delay:.6s}
.boot .hero-copy>.acts{animation-delay:.72s}
.boot .rack{animation:settle 1s var(--ease) .45s both}

/* ---------- mobile: designed, not shrunk ----------
   The light is the signature, so it must survive here rather than be
   switched off. It gets softer and slightly weaker, never removed. */
@media(max-width:640px){
  body{font-size:16px}
  .top-in{height:62px}
  .hero{padding:36px 0 64px}
  .acts .act{flex:1 1 100%}
  /* Softer than desktop, as intended, but nowhere near the old 90/110px.
     A phone is exactly where this cost hurts most, and a blur this large over
     a full viewport was the single most expensive thing on the page here. */
  .beam-wide{filter:blur(30px)}
  .beam-core{filter:blur(38px)}
  .case{padding:60px 0}
  /* On a phone every screenshot IS a phone screenshot, so the desktop browser
     chrome above it would misdescribe what you are looking at. It goes, and the
     frames tighten into something that reads as a handset. The hero device gets
     the portrait ratio of the real captures (360x754) instead of 16/10. */
  .chrome{display:none}
  .case-shot .pane,.rack .pane{border-radius:22px;max-width:280px;margin:0 auto}
  .rack .shots{aspect-ratio:390/820}
  .dials{justify-content:center}
  .plan{border-radius:18px}
  .close-panel{border-radius:20px}
  .qa summary{padding-right:38px}
}

@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition:none!important}
  .js .lift,.js .stagger>*{opacity:1!important;transform:none!important}
  .js .kin .ln i{transform:none!important}
  .pane{transform:none!important}
  .shots img{transition:none}
  /* Every hidden state introduced above is restored explicitly. An animation
     must never be the thing that decides whether content is visible.
     The FAQ restore is scoped to [open] on purpose: forcing block-size on a
     CLOSED details would leave every answer on the page permanently open. */
  .js .plan li::before{transform:rotate(-45deg) scale(1)!important}
  .js .band-hd.lift .eyebrow{opacity:1!important;transform:none!important}
  .qa details[open]::details-content{block-size:auto!important;opacity:1!important}
}


/* ============================================================
   Lead form  ·  added 2026-08-06
   Three steps, contact last, because the contact step is where a form
   is abandoned and every field before it is sunk cost that argues
   against leaving. Step one is a single tap with no typing at all.

   It drinks from the same --lc/--li light properties as everything
   else, so the form tints with the room rather than sitting on it as
   a grey plastic box.
   ============================================================ */

.lf{margin:34px auto 0;max-width:520px;text-align:left}

/* Progress. Three dots, not a percentage bar: a bar invites the question
   "how much is left", three dots answer it before it is asked. */
.lf-prog{display:flex;gap:7px;justify-content:center;margin-bottom:30px}
/* Transition OPACITY, never background.
   The background is derived from --lc, and app.js rewrites --lc every frame,
   which restarts a background transition every frame - so it never visibly
   progresses and the dot stays on its start colour forever. Opacity does not
   depend on --lc, so it animates normally. Only the first dot ever looked
   right, because it is switched on before the light loop starts and therefore
   never transitions at all. */
.lf-prog i{
  width:34px;height:3px;border-radius:999px;
  background:rgb(var(--lc) / .85);
  opacity:.2;
  transition:opacity .45s var(--ease);
}
.lf-prog i.on{opacity:1}

.lf-step{border:0;margin:0;padding:0;min-inline-size:0}
.lf-step[hidden]{display:none}
.lf-step:not([hidden]){animation:rise .5s var(--ease) both}

.lf-q{
  font-family:var(--serif);font-size:clamp(1.3rem,3vw,1.6rem);line-height:1.2;
  letter-spacing:-.01em;padding:0;margin:0 0 22px;display:block;width:100%;
  text-align:center;color:var(--porcelain);
}

/* ---- step 1: the picks ---- */
.lf-picks{display:grid;gap:10px}
.lf-pick{
  display:block;width:100%;text-align:left;cursor:pointer;font:inherit;
  padding:18px 20px;border-radius:16px;color:var(--porcelain);
  background:rgba(242,240,236,.04);border:1px solid var(--edge);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  /* transform only. Colour here is --lc-derived and --lc is rewritten every
     frame by the light loop, so transitioning it restarts the animation each
     frame and the colour never arrives. Same trap as the progress dots. */
  transition:transform .3s var(--ease);
}
.lf-pick b{display:block;font-weight:640;font-size:1.02rem;letter-spacing:-.01em}
.lf-pick small{display:block;margin-top:3px;color:var(--ash);font-size:.88rem}
.lf-pick:hover{
  transform:translateY(-2px);border-color:rgb(var(--lc) / .5);
  background:rgb(var(--lc) / .07);
}
.lf-pick.sel{border-color:rgb(var(--lc) / .8);background:rgb(var(--lc) / .1)}

/* ---- fields ---- */
.lf-lab{
  display:block;margin:20px 0 8px;font-size:.86rem;font-weight:600;
  letter-spacing:-.01em;color:var(--porcelain);
}
.lf-step .lf-lab:first-of-type{margin-top:0}
.lf-opt{font-weight:400;color:var(--ash)}

.lf-in{
  width:100%;font:inherit;font-size:1rem;color:var(--porcelain);
  padding:14px 16px;border-radius:13px;
  background:rgba(8,9,12,.45);border:1px solid var(--edge);
  /* No transition on the focus colours: they are --lc-derived, and a focus ring
     that fades in is worse than one that simply appears. */
  transition:background .25s var(--ease);
}
.lf-in::placeholder{color:var(--ash);opacity:.75}
.lf-in:hover{border-color:rgba(242,240,236,.22)}
.lf-in:focus{
  outline:none;border-color:rgb(var(--lc) / .75);background:rgba(8,9,12,.65);
  box-shadow:0 0 0 3px rgb(var(--lc) / .16);
}
/* Only scold a field the visitor has actually left, never while typing. */
.lf-in.bad{border-color:#e0614a;box-shadow:0 0 0 3px rgba(224,97,74,.16)}
.lf-area{resize:vertical;min-height:88px;line-height:1.55}

/* Beats `.close-panel p{color:var(--ash)}`, which is (0,2,0) and would
   otherwise grey out every error message on the form. Specificity, not
   !important, so this stays overridable. */
.close-panel p.lf-err{margin:8px 0 0;font-size:.84rem;color:#e0614a}

.lf-nav{display:flex;gap:10px;margin-top:26px}
.lf-nav .act{flex:1 1 auto}
.lf-nav .act.hollow{flex:0 0 auto}

.close-panel p.lf-fine{
  margin:16px 0 0;font-size:.8rem;color:var(--ash);text-align:center;
}

/* Honeypot: off-screen rather than display:none, because some bots skip
   hidden inputs and this one is meant to be irresistible. */
.lf-hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

/* ---- the thank-you ---- */
.lf-ok{
  margin:34px auto 0;max-width:520px;padding:30px 26px;border-radius:20px;
  background:rgb(var(--lc) / .08);border:1px solid rgb(var(--lc) / .35);
  animation:rise .6s var(--ease) both;
}
.lf-ok h3{
  font-family:var(--serif);font-size:1.5rem;margin:0 0 12px;letter-spacing:-.01em;
}
.close-panel .lf-ok p{color:var(--porcelain);margin:0 0 18px;max-width:none}

.lf-alt{display:inline-block;margin-top:26px}

@media(max-width:640px){
  .lf-q{font-size:1.22rem}
  .lf-nav{flex-wrap:wrap}
  .lf-nav .act{flex:1 1 100%}
  .lf-nav .act.hollow{order:2}
}
