/* ============================================================
   AtraX — Sections & components
   ============================================================ */

/* ---------------- Navbar ---------------- */
/* A flat bar with a hairline, not a floating glass pill. The blur, the 999px
   radius and the four-layer inset shadow were the loudest "modern SaaS" tell
   on the page. */
.nav {
  position: fixed; top: 14px; left: 50%; z-index: 100;
  width: min(1180px, calc(100% - 32px));
  transform: translateX(-50%);
  border: 1px solid var(--nav-border);
  border-radius: var(--r-md);
  background: linear-gradient(180deg, var(--nav-bg-a), var(--nav-bg-b));
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  box-shadow: none;
  overflow: hidden;
  isolation: isolate;
  transition: background .35s var(--ease, ease), border-color .35s var(--ease, ease);
}
.nav::before { display: none; }
.nav::after { display: none; }
/* The bar is themed ENTIRELY by the tokens, with no dark variant class.

   There used to be one: .navbar--dark, switched on by app.js the moment the
   page's darkness passed 0.55, and it hard-coded the background, the border and
   all four shadow layers. Which meant the bar was fully dark for the whole
   back half of the transition while the page behind it was still on its way -
   at t = 0.6 the page is 60% dark and the bar was at 100%. That is the nav not
   matching the page, literally, and it is the reason the swap read as a snap
   rather than as the page changing mode.

   It could not have tweened either, whatever the threshold: the light state was
   a flat colour and the dark state a gradient, and `transition: background`
   cannot interpolate between the two, so it always swapped in one frame.

   Everything now derives from --theme-t and is continuous by construction, so
   the bar cannot drift out of step with the page. The class is gone from app.js
   too. Both end states are preserved to the value - see --nav-bg-a/b and
   --nav-sh-* in styles.css.

   Also dropped: .navbar--dark::before and ::after, which set backgrounds on two
   pseudo-elements that .nav::before/::after above had already taken out with
   display: none. They had not drawn anything in a long time. */
.nav.scrolled {
  box-shadow:
    0 14px 42px var(--nav-sh-1),
    0 2px 8px var(--nav-sh-2),
    inset 0 1px 0 var(--nav-sh-top),
    inset 0 -1px 0 var(--nav-sh-bot);
}
/* Fallback for browsers without backdrop-filter — the theme-driven --nav-bg
   is already semi-opaque, so the bar stays readable without the blur. */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .nav, .nav.scrolled { background: var(--nav-bg); }
}
@media (prefers-reduced-motion: reduce) {
  .nav { transition: none; }
  .nav-indicator { transition: none; }
}
/* 64px, not 56 (2026-09-24, owner: "improve the design of the navbar"). At
   56 the 38px lockup and a 13px link row sat tight against the hairline; the
   extra 8px is what lets the bar read as a bar rather than a strip. Phones
   keep 56 (the 660 block). */
.nav-inner { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; height: 64px; padding-left: 20px; padding-right: 20px; }
.brand { display: flex; align-items: center; gap: 11px; font-family: var(--font-head); font-weight: 700; font-size: var(--t-xl); letter-spacing: -0.02em; }
.brand { display: flex; align-items: center; gap: 9px; font-family: var(--font-head); font-weight: 700; font-size: var(--t-xl); letter-spacing: -0.012em; }
/* The nav carries the FULL lockup from ATRAX NEW.pdf - mark, wordmark, divider
   and SOFTWARE SOLUTIONS - inline so its dark ink can follow the theme. The
   owner asked for it this way round (2026-09-23); the mark + wordmark alone
   sits at the centre of the Services tech sphere.

   Not the PDF's proportions exactly: at bar size its tagline set at ~7px and
   SOLUTIONS' hairline strokes vanished (owner: "software solutions isn't
   readable"). In this nav copy the two tagline lines are scaled 1.45x about
   their own left edge and the logo's centre line, which still fits inside the
   mark's height, and SOLUTIONS gets a stroke in its own colour to thicken it.
   At 38px tall the tagline caps are ~11px. assets/brand/atrax-logo.svg keeps
   the original proportions. */
.brand-logo { height: 38px; width: auto; display: block; flex: none; }
.lg-thin { stroke: var(--logo-ink); stroke-width: 7; stroke-linejoin: round; }
.lg-ink { fill: var(--logo-ink); }
/* The footer carries the same copy of the lockup. Its ground is always the
   solid --d-bg, so the ink is plain white rather than --logo-ink, which only
   reaches white at --theme-t = 1 and stays slate if the tween never runs. */
.foot-brand .lg-ink { fill: #fff; }
.foot-brand .lg-thin { stroke: #fff; }

/* The links were 13px with 10px sides, a tight cluster in the middle of a
   1180px bar. One step up the scale and 14px sides spread them to a
   comfortable row without changing what sits where. */
.nav-links { position: relative; display: flex; align-items: center; gap: 2px; }
.nav-link { font-size: var(--t-sm); font-weight: 500; color: var(--nav-text); padding: 8px 14px; border-radius: var(--r-sm); position: relative; transition: color .2s, background .2s; }
.nav-link:hover { color: var(--page-text); background: rgba(128,128,132,0.11); }
.nav-link.is-active { color: var(--page-text); }
.nav-indicator {
  position: absolute;
  left: 0;
  bottom: 1px;
  width: 0;
  height: 2px;
  border-radius: var(--r-sm);
  background: var(--accent);
  box-shadow: 0 0 10px rgba(232, 76, 60,.28);
  transform: translate3d(0,0,0);
  transition: transform 320ms cubic-bezier(.22,1,.36,1), width 320ms cubic-bezier(.22,1,.36,1), opacity .2s ease;
  pointer-events: none;
}
:is(section, footer)[id] { scroll-margin-top: 96px; }
.nav-right { display: flex; align-items: center; gap: 14px; }
/* It is a real button now, and specifically THE SAME button as the hero's
   "Plan Your Project": .btn .btn-invert in the markup, so the type, the 2px
   radius, the arrow nudge and the press state all come from the button system
   rather than being restated here. One action, one look, in both places.

   It used to be a bare bold text link with a 7px orange dot in front of it. The
   dot was doing the work a button's own shape should do — saying "this is the
   action" — and once there is a fill it is just clutter, so it has gone.

   All this rule keeps is the nav's tighter padding: the hero's 16px/24px makes
   a 44px button, which is too tall for the bar. */
.nav-cta-link { padding: 12px 20px; }

/* Bare lines, no box (2026-09-24). The bordered grey tile read as a form
   control next to a solid Book a call; the hit area is unchanged. */
.nav-burger { display: none; width: 38px; height: 38px; border: 0; border-radius: var(--r-sm); background: transparent; cursor: pointer; position: relative; }
.nav-burger span { position: absolute; left: 11px; right: 11px; height: 2px; background: var(--page-text); border-radius: 2px; transition: transform .3s, opacity .2s; }
.nav-burger span:nth-child(1) { top: 15px; } .nav-burger span:nth-child(2) { top: 21px; } .nav-burger span:nth-child(3) { top: 27px; }
body.menu-open .nav-burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
body.menu-open .nav-burger span:nth-child(2) { opacity: 0; }
body.menu-open .nav-burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.mobile-menu { position: fixed; top: 86px; left: 50%; width: min(560px, calc(100% - 32px)); z-index: 99; background: color-mix(in srgb, var(--nav-bg) 94%, transparent); backdrop-filter: blur(20px) saturate(150%); -webkit-backdrop-filter: blur(20px) saturate(150%); border: 1px solid var(--nav-border); border-radius: var(--r-md); box-shadow: none; padding: 16px 20px 22px; transform: translate(-50%,-135%); transition: transform .4s cubic-bezier(.2,.7,.2,1); display: none; }
.mobile-menu a { display: block; padding: 14px 4px; font-size: var(--t-lg); font-weight: 500; border-bottom: 1px solid var(--nav-border); color: var(--page-text); }
.mobile-menu a:not(.btn) { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-display); font-size: var(--t-xl); letter-spacing: -0.012em; }
/* The page you are on, marked the way the desktop bar marks it: in accent. */
.mobile-menu a.is-active { color: var(--accent); }
.mobile-menu a.is-active::after { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
/* The menu's Book a call is .btn-primary in the shell, and --cta-gradient
   has been `none` since the gradients went, while .mobile-menu a sets the
   ink - so it rendered as plain dark text with a rule under it. It takes
   the CTA band's solid accent here instead. */
.mobile-menu .btn { display: flex; justify-content: center; margin-top: 20px; width: 100%; background: var(--accent); color: #fff; border: 0; padding: 16px 20px; font-size: var(--t-sm); }
body.menu-open .mobile-menu { transform: translate(-50%,0); }
/* Closed, the menu is only translated off-screen, and its blur(20px) layer was
   still composited on every scrolled frame. Hidden once it has slid away. */
.mobile-menu { visibility: hidden; transition: transform .4s cubic-bezier(.2,.7,.2,1), visibility 0s linear .4s; }
body.menu-open .mobile-menu { visibility: visible; transition: transform .4s cubic-bezier(.2,.7,.2,1), visibility 0s; }

/* ---------------- Hero (centered, light, airy) ---------------- */
.hero {
  --hero-pad-top: 146px;
  position: relative; background: transparent; color: var(--ink);
  overflow: hidden; display: flex; align-items: flex-start;
  text-align: center; padding: var(--hero-pad-top) 0 80px;
}
@media (max-height: 900px) { .hero { --hero-pad-top: 112px; padding-bottom: 56px; } }
html[data-hero="dark"] .hero { background: var(--d-bg); color: var(--d-ink); }
/* A warm bloom behind the copy — warm rather than cool, since a cool bloom on
   white just reads as grey. It used to hug the far-left corner, because the
   copy sat left and the globe video held the right half; with the headline now
   centred it has moved inboard to sit behind the text, matching the soft
   upper-left wash on atrax-website.vercel.app. */
.hero-glow { position: absolute; pointer-events: none; z-index: 1; inset: 0;
  background: radial-gradient(58% 62% at 26% 6%, rgba(255, 150, 110, 0.16), transparent 70%); }
html[data-hero="dark"] .hero-glow { background: none; }
.hero-fade { position: absolute; left: 0; right: 0; bottom: 0; height: 160px; z-index: 1; pointer-events: none; background: linear-gradient(to bottom, transparent, #ffffff); }
html[data-hero="dark"] .hero-fade { background: linear-gradient(to bottom, transparent, var(--d-bg)); }

/* Text-only hero. The globe used to live here, pinned to the right and
   bleeding off the edge; it now has a section of its own below, so the copy
   is free to run the full width of the wrap at every breakpoint. */
.hero-inner { position: static; width: 100%; max-width: var(--maxw); margin: 0 auto; display: block; }
/* The copy column fills the first screen so the hero is a full viewport. */
.hero-copy { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
  min-height: calc(100svh - var(--hero-pad-top) - 80px); }
.hero-copy .hero-cta { justify-content: center; }
.hero-copy .hero-sub { margin-left: auto; margin-right: auto; max-width: 620px; }


/* Type metrics follow atrax-website.vercel.app — leading of exactly 1 and
   -0.045em of tracking — but not its size. That reference sets a 19-character
   headline at 9vw; this one is 68 characters over three lines, and the longest
   line has to fit the wrap at every width. 7.2vw lands it at 95px on a desktop.

   The floor is 24px, not 30px, and the vw term is what carries the narrow end.
   At a fixed 30px the longest line cleared a 375px screen by six pixels and
   wrapped on anything narrower — a 360px phone is common and 320px still
   exists. Letting the size track the viewport keeps a line's width a constant
   fraction of the screen, so it fits all the way down instead of falling off a
   cliff at one width. The size is deliberately off the --t-display token —
   that drives section headings across the site, and this runs larger. */
.hero h1 { font-family: var(--font-display); font-size: clamp(24px, 7.2vw, 95px); font-weight: 700; margin: 0; line-height: 1.02; letter-spacing: -0.045em; color: var(--ink); }
html[data-hero="dark"] .hero h1 { color: #fff; }
.hero h1 .h-line { display: block; }
/* The caret rides whichever line typewriter() is currently filling. steps(1)
   so it blinks on and off like a terminal cursor rather than pulsing. */
.hero h1 .h-line.is-typing::after {
  content: ""; display: inline-block; vertical-align: -0.05em;
  width: .05em; height: .72em; margin-left: .06em;
  background: var(--accent);
  animation: heroCaret 1s steps(1, end) infinite;
}
@keyframes heroCaret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .hero h1 .h-line.is-typing::after { display: none; } }

.hero-sub { margin-top: 36px; margin-left: auto; margin-right: auto; font-size: var(--t-lede); color: var(--ink-2); max-width: 620px; line-height: 1.55; }
html[data-hero="dark"] .hero-sub { color: var(--d-ink-2); }
.hero-cta { margin-top: 38px; display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; }
/* hero button theming for dark mode */
html[data-hero="dark"] .hero .btn-solid { background: #fff; color: var(--d-bg); }
html[data-hero="dark"] .hero .btn-ghost { color: #fff; border-color: var(--d-line); }
html[data-hero="dark"] .hero .btn-ghost:hover { border-color: rgba(255,255,255,.5); }

/* ---------------- Reach (the scroll-scrubbed globe) ----------------
   A tall track with a sticky stage inside it. The track is the scroll budget:
   the stage stays pinned for (track height - one viewport) of scrolling while
   globe.js scrubs the point cloud from a world into the AtraX mark. Change the
   track height and you change how long the morph takes to play - nothing in
   the JS is measured in pixels. */
/* Two classes on purpose, not one. The responsive `.section { padding: 84px 0 }`
   further down this file is also a single class, so below 860px it was beating
   a plain `.reach` on source order and quietly handing this section 84px of
   padding at both ends — 168px of white wrapped around a stage that is already
   empty at both ends by design. `.section.reach` outranks it at every width. */
.section.reach {
  padding: 0;
  /* The stage is a fixed 100vh with the globe cropped at its bottom edge, so
     the band above the cap (376px at 1440x900, 474px on a 375px phone) and the
     band under the finished mark are empty by construction. That emptiness IS
     this section's spacing. Sitting it on top of the hero's trailing space and
     the logo strip's lead stacked two gaps into one hole, which is what read
     as the section floating away from its neighbours.

     So the section is pulled into them instead, by less than the neighbour
     itself has to spare, so nothing of theirs is ever covered: the dead band
     overlaps the gap rather than adding to it.

     The top pull has to be earned. What it eats is the hero's trailing space,
     and that is mostly the slack left over when the centred copy does not fill
     its min-height — which exists on a tall window and vanishes on a short one
     (175px at 1440x900, 75px at 1440x700, 198px at 375x812). So it is measured
     in vh with an offset that holds it at zero until the viewport is tall
     enough to have produced any, and it never claims all of what it finds.

     It is deliberately small. A larger pull does close the gap, but it also
     lifts the section into the FIRST SCREEN, and the globe's heading then peeks
     under the hero before you have scrolled at all — at 130px it was showing
     154px of the next section. The hero is meant to be its own screen.

     30px is under the reach heading's own top padding at every size, so the
     section's box may cross the fold but its first ink never does. The space
     left below the hero's buttons is not a gap being tolerated: it is the
     hero's own centred breathing room, and it reads as composition rather than
     as a hole precisely because nothing else intrudes on it.

     The bottom pull is a flat 64px because the band under the finished mark is
     the shortest thing here (77px at 1440x700, 212px on a phone) and the logo
     strip's own top padding is 72px there and 84px below 860px — so 64 is
     under both at every size, and the strip's first ink still lands after the
     track ends rather than over the mark. */
  margin-top: -30px;
  margin-bottom: -64px;
}
/* Only the globe sticks; the heading scrolls up past it and away.

   Pinning the whole stage froze the entire screen — heading and all — and that
   total absence of movement is what read as the scroll being taken. Leaving
   nothing pinned fixed that but capped the animation: with the globe travelling
   with the page, it is only framed correctly for the instant the section fills
   the viewport, so the morph had about 360px to happen in and was gone.

   Sticking the canvas alone keeps the framing constant for the whole track,
   which gives the morph room, while the heading moving through — and then the
   next section arriving — keeps the page visibly alive. Track height minus one
   viewport is how much of that there is: 100vh here. */
.reach-track { position: relative; height: 200vh; }
/* pointer-events: none because the stage now overhangs the hero above and the
   logo strip below (see the margins on .section.reach). Nothing in here wants a
   click — globe.js binds no handlers to the canvas and the pins opt out
   already — and without this the empty top of the stage would sit over the
   hero as an invisible hit target. */
/* The top edge is faded rather than cut. The arcs leaving Tirana rise well
   above the globe's own crown — higher than the stage is tall — and overflow:
   hidden, which is here to stop the pins and the cloud spilling into the hero
   and the logo strip, was slicing them off against a dead straight horizontal
   line. Softening just that edge keeps the clipping without it being visible.
   Bottom and sides stay hard, because nothing reaches them. */
/* z-index 5 rather than 1. The section is pulled up into the hero, so the
   hero's own layers — the bottom fade at z1, the copy at z2 — now overlap the
   top of this stage. Paint order already favoured the globe, since it is later
   in the document and the fade shares its z-index, but that is a fragile thing
   to rely on: add one stacking context in the hero and the white fade lands on
   top of the arcs with a hard edge where its box ends. 5 clears the hero
   outright. Safe against the copy too, because the band of the stage that
   reaches that high is masked to transparent anyway. */
.reach-globe {
  position: sticky; top: 0; height: 100vh; z-index: 5;
  overflow: hidden; pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 13%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 13%);
}

/* Absolute against the track, so it scrolls with it rather than with the
   sticky globe underneath.

   7vh looks timid next to the 178px of white this leaves between the copy and
   the cap at 1440x900, and it is tempting to drop the heading toward the globe.
   Don't, not here: on a landscape stage the cap sits at (height - 0.92 x the
   radius) and the radius is driven by WIDTH, so the band collapses as the
   window gets wider or shorter — 176px at 1440x700, 99px at 1920x800, where
   even today's value already has the heading over the crown. A CSS length can
   only track that by restating globe.js's radius maths, which would then go
   quietly wrong the first time that number is retuned. The phone rule below is
   the one place the band is predictable enough to spend. */
.reach-head { position: absolute; top: 0; left: 0; right: 0; z-index: 2; padding-top: clamp(48px, 7vh, 88px); margin-bottom: 0; max-width: none; }
/* the headline sets its own two lines with a <br>; this only stops a very
   narrow stage from breaking it into four */
.reach-head h2 { max-width: 22ch; }

.reach-globe canvas { display: block; width: 100%; height: 100%; }

/* Pins are positioned from the projected marker coordinates every frame, so
   the only thing CSS owns here is how they look. transform and opacity only:
   both stay off the main thread. */
.reach-pins { position: absolute; inset: 0; list-style: none; margin: 0; padding: 0; pointer-events: none; }
.reach-pin {
  position: absolute; top: 0; left: 0; will-change: transform, opacity;
  display: flex; align-items: center; gap: 8px; white-space: nowrap;
  margin: -14px 0 0 14px;
  padding: 6px 12px 6px 10px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  font-family: var(--font-mono); font-size: var(--t-3xs);
  letter-spacing: .07em; text-transform: uppercase; color: var(--ink);
}
/* a pin on the right-hand side of the globe hangs its label to the left, or it
   would run off the edge of the stage */
.reach-pin.is-flipped { margin-left: -14px; transform-origin: right center; flex-direction: row-reverse; }
.reach-pin.is-flipped { translate: -100% 0; }
.reach-pin i { width: 6px; height: 6px; flex: none; border-radius: 50%; background: var(--accent-bright); box-shadow: 0 0 0 3px var(--accent-soft); }
/* our own two offices read a step louder than the client pins */
.reach-pin.is-home { color: var(--accent); border-color: var(--accent-line); }
.reach-pin.is-home i { box-shadow: 0 0 0 4px var(--accent-soft); }

@media (max-width: 860px), (pointer: coarse) {
  /* No sticky range at all here: the track is exactly as tall as the stage, so
     the globe never pins and the page never stops answering a flick. That is
     the whole point — globe.js reads this same breakpoint and switches the
     morph from a scrub to a timeline, and a scrub is the only thing the 75vh
     of pinning that used to be here was paying for. Left in place it would be
     75vh of dead scroll held open over an animation that had already finished.

     Keep this in step with PLAY_MQ in globe.js. The two decide the same thing
     from opposite sides and only work together.

     svh, with vh as the fallback: 100vh is the viewport with the toolbar
     hidden, so on first view the bottom of the stage - where the mark
     settles - sat under the toolbar. svh is static too, so it does not
     resize the canvas as the toolbar moves. */
  .reach-track { height: 100vh; height: 100svh; }
  .reach-globe { height: 100vh; height: 100svh; }
  /* A deeper pull than the 64px the other breakpoints take. The band under the
     settled mark is at its emptiest here — the mark sits at 0.52 of the stage
     and the stage runs to 1.0 — and the logo strip's own top padding is 84px
     below this breakpoint, so 80 is still under it and the strip's first ink
     still lands clear of the stage. Measured at 375x812 it leaves 32px between
     the bottom of the globe and the first logo. */
  .section.reach { margin-bottom: -80px; }
  /* Sits higher than it used to, for two reasons that point the same way.

     It closes some of the white between the hero's buttons and this heading,
     which on a phone was 247px of nothing. And it gets the text out of the
     ARCS. Those rise about 0.30 of the globe's radius above its crown — 95px at
     375x812 — and at 11vh the heading occupied stage rows 105 to 160 while the
     arcs reached row 99, so they ran straight through the words at full
     opacity. (The stage's top mask is fully opaque by row 106, so it was not
     softening any of it.) The globe drops to meet this halfway; see
     CROWN_AT_PORT in globe.js. Neither move alone was enough.

     The 32px floor is for a phone held sideways, where the stage is landscape,
     the radius is height-capped instead and the crown rides high. */
  .reach-head { padding-top: clamp(32px, 7vh, 80px); }
  /* no blur behind the one labelled pin either: it moves every frame over a
     canvas that redraws every frame, the most expensive place for one */
  .reach-pin { font-size: 9px; padding: 5px 9px 5px 8px; backdrop-filter: none; -webkit-backdrop-filter: none; background: var(--bg); }

  /* Client pins lose their labels here and become plain dots. At 375px the US
     markers sit within a few dozen pixels of each other, and six pills fighting
     for that corner stacked into an overlapping column against the left edge
     with the rightmost one cut off. Tirana keeps its label, which is the one
     thing actually worth naming on a phone.

     The name is hidden rather than removed, so it is still announced. Stripping
     the pill also means the dot must be re-centred on the marker: the pill hung
     it off one corner, a bare dot sits on the point. Flipping is meaningless
     without a label to flip, so it is cancelled too.
     globe.js reads the same breakpoint and stops de-crowding these, or it would
     shove the dots off the places they mark. */
  .reach-pin:not(.is-home) {
    margin: -3px 0 0 -3px;
    padding: 0; border: 0; background: none;
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .reach-pin:not(.is-home).is-flipped { margin-left: -3px; translate: none; }
  .reach-pin:not(.is-home) > span {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* one static frame, so there is no reason to hold a viewport of scroll open
     for a morph that will not run */
  .reach-track { height: 100vh; }
}

/* ---------------- CSS Mockups (project thumbnails) ---------------- */
/* ---------------- Stats band ---------------- */
.stats { background: var(--bg); position: relative; padding: var(--s-22) 0; }

/* Direction A — the stats stop being four floating cards and become one
   hairline-ruled row: a single top rule, vertical hairlines between cells,
   number in the display face, label in mono. The blurred radial blobs that
   used to sit behind this are gone; the rules do the work instead. */
/* The top rule is a sibling element rather than a border on the grid so it can
   draw itself (data-motion="sweep") while the four numbers count up. Same 1px
   in the same place; it just gets to be a thing that moves. */
.stats-rule { height: 1px; background: var(--line); transform-origin: left center; }
.stats-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0;
  align-items: stretch;
}
.stat {
  padding: var(--s-7) var(--s-7) var(--s-8) 0;
  border-right: 1px solid var(--line);
  transition: background .4s var(--ease);
}
.stat:not(:first-child) { padding-left: var(--s-7); }
.stat:last-child { border-right: 0; padding-right: 0; }
.stat .n {
  font-family: var(--font-head);
  font-size: var(--t-h2);
  font-weight: 800;
  letter-spacing: var(--ls-display);
  line-height: 1;
  color: var(--page-text);
}
.stat .n .suf { color: var(--accent); font-weight: 700; }
.stat .l {
  margin-top: var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--page-text-3);
  transition: color .4s var(--ease);
}
.stat:hover .l { color: var(--page-text-2); }

/* The fifth column is a link, the other four are not. It inherits every .stat
   rule and only adds what a link needs. Kept as a .stat rather than a row
   beneath the band so the rating reads at the same rank as the numbers it sits
   with, which is what was asked for. */
/* The Clutch badge (2026-09-23), after the "Reviewed on Clutch" badge the
   owner sent, for AtraX's own profile: 5.0 from 22 reviews, read off
   clutch.co/profile/atrax that day. Static, so the count is a snapshot.
   Update it by hand, or swap for Clutch's own widget script if a live count
   is ever worth a third-party request. White card, like Clutch's; on light
   ground (About) it takes a hairline so it doesn't dissolve into the page. */
.clutch-badge {
  display: inline-grid; grid-template-columns: auto auto; align-items: center; gap: var(--s-4);
  padding: 10px 14px; background: #fff; color: #17313b; border-radius: var(--r-sm);
  font-family: var(--font-body); line-height: 1.1; text-decoration: none;
  transition: transform .25s var(--ease), box-shadow .25s var(--ease);
}
.clutch-badge:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,.18); }
.clutch-badge--line { border: 1px solid var(--page-line); }
.clutch-badge--line:hover { box-shadow: 0 6px 18px rgba(17,17,20,.08); }
.cb-l, .cb-r { display: grid; gap: 3px; }
.cb-k { font-size: 9px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #3d5560; }
.cb-w { font-family: var(--font-display); font-size: 22px; font-weight: 700; letter-spacing: -0.02em; color: #17313b; }
.cb-s { font-size: 15px; letter-spacing: 1px; color: var(--accent); }
.cb-n { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #3d5560; }
.foot-brand .clutch-badge { margin-top: var(--s-5); }
.stat-badge { display: flex; align-items: center; }
.stat-cred { color: inherit; display: block; }
.stat-cred .n .suf { margin-left: 2px; }
.stat-cred .arr { display: inline-block; transition: transform .25s var(--ease); }
.stat-cred:hover .arr { transform: translate(2px, -2px); }
.stat-cred:hover .l { color: var(--accent); }
.stat-cred:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r-sm); }

/* ---------------- What we do ----------------
   Four numbered services in ONE bordered row divided by vertical hairlines,
   not four floating cards: the rule does the dividing, the way every other
   band on this site works. */
.services { background: var(--bg); }
/* one word of the headline carries the accent; <em> for the emphasis, not the italic */
.services .section-head h2 em { font-style: normal; color: var(--accent); }

.wwd-row {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--bg);
  overflow: hidden;
}
.wwd {
  display: flex; flex-direction: column;
  padding: var(--s-8) var(--s-6) var(--s-10);
  border-left: 1px solid var(--line);
  transition: background .4s var(--ease);
}
.wwd:first-child { border-left: 0; }
.wwd:hover { background: var(--line-soft); }
.wwd-n { font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: var(--ls-mono); color: var(--accent); }
.wwd h3 { margin-top: var(--s-5); font-size: var(--t-xl); font-weight: 700; letter-spacing: -0.02em; text-wrap: balance; }
.wwd > p { margin-top: var(--s-3); font-size: var(--t-sm); line-height: 1.55; color: var(--ink-2); }
/* the bullets are pushed to the bottom so all four lists align across the row
   however long the descriptions above them run */
.wwd ul { list-style: none; display: grid; gap: var(--s-3); margin-top: auto; padding-top: var(--s-6); }
.wwd ul::before { content: ""; display: block; height: 1px; margin-bottom: var(--s-5); background: var(--line); }
.wwd li { position: relative; padding-left: 16px; font-size: var(--t-s); line-height: 1.45; color: var(--ink-2); }
.wwd li::before { content: ""; position: absolute; left: 0; top: 7px; width: 5px; height: 5px; border-radius: 50%; background: var(--ink-4); transition: background .3s var(--ease); }
.wwd:hover li::before { background: var(--accent); }

/* ---------------- Selected work ----------------
   An explicit grid, not the old `columns: 2` multicol: one tall card left, two
   stacked right, one band across the bottom. DOM order and visual order agree
   now, so the reveal delays just increment - no more staggering by column. */
.projects { background: var(--bg); }
.work-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--s-4) var(--s-6); margin-bottom: var(--s-14); }
.work-head > .eyebrow { grid-column: 1 / -1; }
/* "Trusted by companies worldwide." under the heading (reviewer, 2026-09-23);
   the All projects link keeps the right-hand column and sits on its baseline. */
.work-sub { grid-column: 1; grid-row: 2; margin: 0; font-size: var(--t-xl); line-height: 1.5; color: var(--page-text-2); }
.work-head > h2 { grid-column: 1; grid-row: 1; }
.work-head > .work-all { grid-column: 2; grid-row: 1 / span 2; align-self: end; }
.work-head h2 { font-family: var(--font-display); font-size: var(--t-h2); font-weight: 600; margin-top: var(--s-4); line-height: 1.1; letter-spacing: -0.012em; color: var(--page-text); }
.work-all {
  display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;
  font-family: var(--font-mono); font-size: var(--t-xs); letter-spacing: var(--ls-mono); text-transform: uppercase;
  color: var(--ink-2); padding-bottom: 7px; border-bottom: 1px solid var(--line);
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.work-all:hover { color: var(--accent); border-color: var(--accent); }
.work-all .arr { transition: transform .25s var(--ease); }
.work-all:hover .arr { transform: translate(2px,-2px); }

/* Four cards two-up, then one full-width band. On 2026-09-23 the reviewer
   swapped Dextrio for Siemplify and Seraphic; for a few hours that made six in
   a 3x2 grid, and the owner cut Conversation platform to get back to five and
   this shape. (.wk-e / .wk-f are left from that and unused.)
   Before that: this was a 1.41fr/1fr split
   with a portrait plate spanning both rows on the left - and that plate was a
   deliberate thing, kept "reading as a portrait plate rather than as half of a
   two-up". It could not survive a fourth card: three stacked 3/2 cards in the
   right column run about 260px taller than a 3/4 plate beside them, and the
   only ways to close that are to stretch the plate to ~0.59 (rejected once
   already: the extra height just becomes dead tint) or to leave a hole.
   Equal columns instead, one ratio for all four. If the plate is wanted back,
   it is a three-card composition again. */
.work { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
.work-item { display: flex; flex-direction: column; min-width: 0; color: inherit; text-decoration: none; }
.wk-a { grid-column: 1; grid-row: 1; }
.wk-b { grid-column: 2; grid-row: 1; }
.wk-c { grid-column: 1; grid-row: 2; }
.wk-d { grid-column: 2; grid-row: 2; }
.wk-e { grid-column: 1; grid-row: 3; }
.wk-f { grid-column: 2; grid-row: 3; }
.wk-wide { grid-column: 1 / -1; }
.work-thumb { position: relative; border-radius: var(--r-lg); overflow: hidden; background: var(--bg); }
/* 3:2, matching the gallery screenshots these cards carry (~1.45-1.50). They
   used to hold the square card thumbs, which are crops — content runs off the
   left and right edges of the 740x740 canvas — in a 4:3 frame that forced them
   down to 0.38 of their own size. The gallery frames are whole, and at 1024
   wide they still downscale here, which is what keeps them sharp. */
.wk-a .work-thumb, .wk-b .work-thumb,
.wk-c .work-thumb, .wk-d .work-thumb,
.wk-e .work-thumb, .wk-f .work-thumb { aspect-ratio: 3 / 2; }
.wk-wide .work-thumb { aspect-ratio: 21 / 8; }
.work-thumb .scene { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 26px; transition: transform .8s cubic-bezier(.2,.7,.2,1); }
.work-item:hover .work-thumb .scene { transform: scale(1.04); }
.work-thumb .scene.dark { background: radial-gradient(120% 120% at 70% 10%, #1c1f26, var(--d-bg)); }
.work-thumb .scene.lite { background: linear-gradient(140deg, #ffffff, #eceeec); }
.work-thumb .scene.cool { background: linear-gradient(140deg, #f3f4ff, #e8ebfb); }
.work-thumb .scene.warm { background: linear-gradient(140deg, #fbfbfa, #eef0ee); }

/* Title, descriptor and year underneath. One wrapping flex row rather than a
   media query: on the wide band all three fit on a line, and on a half-width
   card the descriptor's 320px basis pushes it onto its own line by itself. */
.work-meta { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--s-6); row-gap: 8px; margin-top: 20px; }
.work-name { flex: none; font-family: var(--font-display); font-size: var(--t-h2s); font-weight: 600; letter-spacing: -0.012em; line-height: 1.08; transition: color .25s var(--ease); }
.work-desc { flex: 1 1 320px; min-width: 0; font-size: var(--t-sm); line-height: 1.55; color: var(--ink-2); }
.work-year { flex: none; margin-left: auto; font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: var(--ls-mono); color: var(--ink-4); }
.work-item:hover .work-name { color: var(--accent); }

/* ---------------- Engagement models ---------------- */
.engage { background: var(--bg); }
.eng-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 18px; }
.eng { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 26px 22px 28px; display: flex; flex-direction: column; gap: 12px; background: var(--bg); position: relative; overflow: hidden; transition: transform .3s, box-shadow .3s, border-color .3s; min-height: 230px; }
.eng:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); border-color: rgba(20,22,26,0.16); }
.eng .ix { font-family: var(--font-mono); font-size: var(--t-xs); color: var(--ink-4); }
.eng h3 { font-size: var(--t-xl); font-weight: 700; }
.eng p { font-size: var(--t-sm); color: var(--ink-2); line-height: 1.5; }
.eng ul { list-style: none; display: grid; gap: 8px; margin-top: 2px; }
.eng li { font-size: var(--t-s); color: var(--ink-2); padding-left: 18px; position: relative; }
.eng li::before { content: ""; position: absolute; left: 0; top: 7px; width: 6px; height: 6px; border-radius: 2px; background: var(--ink-4); }
.eng.feature { background: var(--d-bg); color: var(--d-ink); border-color: transparent; }
.eng.feature h3 { color: #fff; } .eng.feature p { color: var(--d-ink-2); } .eng.feature li { color: var(--d-ink-2); }
.eng.feature li::before { background: var(--d-ink-3); }
.eng.feature .ix { color: var(--d-ink-2); }
.eng.feature .badge { position: absolute; top: 20px; right: 20px; font-family: var(--font-mono); font-size: var(--t-3xs); letter-spacing: .08em; text-transform: uppercase; color: var(--d-ink-2); border: 1px solid var(--d-line); border-radius: var(--r-sm); padding: 4px 9px; }

/* ---------------- Testimonials (infinite carousel) ---------------- */
.testi { background: var(--bg); overflow: hidden; }
.testi .section-head { margin-bottom: 48px; }

/* ---------------- Clients (logo marquee) ---------------- */
.clients { background: var(--bg); overflow: hidden; padding: 72px 0; }
.clients .section-head { margin-bottom: 28px; }
.logos-carousel { position: relative; width: 100%; }
.logos-carousel { -webkit-mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%); mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%); }
.logos-viewport { overflow: hidden; cursor: grab; touch-action: pan-y; -webkit-user-select: none; user-select: none; outline: none; }
.logos-viewport.dragging { cursor: grabbing; }
.logos-viewport:focus-visible { box-shadow: inset 0 0 0 2px var(--accent-line); border-radius: var(--r-md); }

.logos-track { display: flex; align-items: center; gap: clamp(46px, 5.6vw, 84px); width: max-content; will-change: transform; padding: 8px 0; }

.logo-item { flex: none; display: grid; place-items: center; height: 64px; }
/* Full colour, at rest and on hover. The marks used to be desaturated to 72%
   and only resolve under the cursor; a client wall reads as proof, and proof
   should not have to be hovered for. Hover is now a lift and nothing else. */
.logo-item img {
  max-height: 40px; width: auto; max-width: 180px; display: block;
  filter: none; opacity: 1;
  transition: transform .35s var(--ease, ease);
  -webkit-user-drag: none; user-select: none; pointer-events: none;
}
.logo-item.lg img { max-height: 56px; }
.logo-item:hover img { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .logos-viewport { overflow-x: auto; cursor: default; -webkit-overflow-scrolling: touch; }
  .logos-carousel { -webkit-mask-image: none; mask-image: none; }
}

@media (max-width: 660px) {
  .logo-item { height: 52px; }
  .logo-item img { max-height: 32px; max-width: 140px; }
  .logo-item.lg img { max-height: 46px; }
}


.testi-carousel { position: relative; width: 100%; }
.testi-carousel { -webkit-mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%); mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%); }
.testi-viewport { overflow: hidden; cursor: grab; touch-action: pan-y; -webkit-user-select: none; user-select: none; }
.testi-viewport.dragging { cursor: grabbing; }
.testi-viewport:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; border-radius: var(--r-sm); }
.testi-track { display: flex; align-items: stretch; gap: 24px; width: max-content; padding: 16px var(--gutter); will-change: transform; }

.quote { background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 30px 28px; margin: 0; display: flex; flex-direction: column; gap: 18px; flex: 0 0 clamp(280px, 80vw, 384px); height: 356px; box-sizing: border-box; transition: transform .28s cubic-bezier(.2,.7,.2,1), box-shadow .28s ease, border-color .28s ease; }
/* The fixed height above holds every card to one size on the desktop. On a
   phone the card is ~290px wide and the longest quote needs ~410px of height,
   so the name row spilled past the border and the viewport clipped it. Below
   860 the cards grow instead; align-items: stretch on the track keeps them
   one height as a set. */
@media (max-width: 860px) { .quote { height: auto; min-height: 356px; } }
.testimonial-logo-wrap { width: auto; height: 48px; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; display: flex; align-items: center; justify-content: flex-start; flex: none; }
.testimonial-company-logo { display: block; width: auto; max-width: 120px; height: auto; max-height: 42px; object-fit: contain; object-position: left center; }
.testimonial-company-logo.logo-wide { max-width: 120px; max-height: 34px; }
.testimonial-company-logo.logo-square { max-width: 58px; max-height: 48px; }
.testimonial-logo-fallback { width: 48px; justify-content: flex-start; padding: 0; font-family: var(--font-mono); font-size: var(--t-xs); font-weight: 600; letter-spacing: .08em; color: var(--ink-2); }
.quote blockquote { margin: 0; font-family: var(--font-head); font-weight: 500; font-size: var(--t-md); line-height: 1.52; letter-spacing: -0.01em; color: var(--ink); flex: 1; text-wrap: pretty; }
.quote .who { display: flex; align-items: center; gap: 12px; margin: 0; }
.quote .av { width: 44px; height: 44px; border-radius: 50%; background: linear-gradient(135deg, var(--bg-muted), #d7d7d1); flex: none; display: grid; place-items: center; font-family: var(--font-head); font-weight: 600; color: var(--ink-2); font-size: var(--t-base); overflow: hidden; }
.quote .av img { width: 100%; height: 100%; object-fit: cover; display: block; }
.quote .who .nm { font-weight: 600; font-size: var(--t-sm); font-family: var(--font-body); }
.quote .who .rl { font-size: var(--t-xs); color: var(--ink-4); margin-top: 2px; }
.quote .who .co { color: var(--ink-2); font-weight: 600; }

.testi-viewport:not(.dragging) .quote:hover { transform: translateY(-6px); box-shadow: var(--shadow-md); border-color: #d8d8dc; }

@media (prefers-reduced-motion: reduce) {
  .testi-carousel { -webkit-mask-image: none; mask-image: none; }
  .testi-viewport { overflow-x: auto; cursor: default; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
  .testi-track { width: max-content; }
  .quote { scroll-snap-align: center; }
}

/* ---------------- Final CTA ---------------- */
/* The closing band, rebuilt to the reviewer's mockup (2026-09-23): eyebrow,
   accent second line, three points, accent Book a call, three checks, a
   handwritten aside and a labelled rule. It is on every page, so every colour
   here is a --page-* token: the band is reached mid-tween, not on a flat dark. */
.cta { background: var(--d-bg); color: var(--d-ink); position: relative; overflow: hidden; }
/* It paints nothing (background: none) and used to carry a 30px blur anyway:
   a 640px filter layer composited on every page for no pixels. */
.cta-glow { position: absolute; left: 50%; top: 42%; transform: translate(-50%,-50%); width: 640px; height: 640px; background: none; pointer-events: none; }
/* Top and bottom only. The shorthand this used to be (120px 0 72px) zeroed
   .wrap's own side gutter, so on a phone the heading, text, buttons and rule
   ran to the screen edge - on every page. */
.cta .wrap { position: relative; z-index: 2; text-align: center; padding-top: 120px; padding-bottom: 72px; }
.cta-eyebrow { letter-spacing: .24em; margin-bottom: var(--s-6); }
.cta h2 { font-size: var(--t-h1); color: var(--page-text); text-wrap: balance; }
.cta h2 em { font-style: normal; color: var(--accent); }
.cta p { margin: 26px auto 0; max-width: 640px; color: var(--page-text-2); font-size: var(--t-lg); line-height: 1.5; }

.cta-points {
  list-style: none; margin: 48px auto 0; padding: 0; display: flex; justify-content: center;
}
.cta-points li {
  display: flex; align-items: center; gap: var(--s-4); text-align: left;
  padding: 0 clamp(var(--s-6), 3.4vw, var(--s-12));
}
.cta-points li + li { border-left: 1px solid var(--page-line); }
.cta-points svg { width: 30px; height: 30px; flex: none; color: var(--accent); }
.cta-points span { display: grid; gap: 2px; font-size: var(--t-sm); color: var(--page-text-2); }
.cta-points b { font-family: var(--font-display); font-size: var(--t-base); font-weight: 600; color: var(--page-text); }

.cta-btns { margin-top: 48px; display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
.cta-btns .btn { font-size: var(--t-sm); padding: var(--s-5) var(--s-10); }

.cta-checks {
  list-style: none; margin: 32px 0 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--s-3) var(--s-10);
}
.cta-checks li { position: relative; padding-left: 26px; font-size: var(--t-sm); color: var(--page-text-2); }
/* the tick is two borders of a rotated box, so it takes the accent without an
   SVG per item */
.cta-checks li::before {
  content: ""; position: absolute; left: 3px; top: .2em; width: 6px; height: 11px;
  border: solid var(--accent); border-width: 0 2px 2px 0; transform: rotate(45deg);
}

/* The handwritten aside. Decorative (aria-hidden) and desktop-only: it sits in
   the right margin beside the buttons, and there is no margin below 1180. */
.cta-note {
  position: absolute; right: 0; top: 50%; width: 170px; color: var(--page-text-2);
  transform: translateY(-10%); pointer-events: none;
}
.cta-note span {
  display: block; font-family: "Caveat", cursive; font-weight: 500; font-size: 27px;
  line-height: 1.12; transform: rotate(-12deg); text-align: left;
}
.cta-note svg { width: 84px; height: 76px; display: block; margin: 8px 0 0 10px; }

/* the labelled rule that closes the band */
.cta .wrap.cta-foot {
  padding-top: 0; padding-bottom: 56px; display: flex; align-items: center; gap: var(--s-6);
  font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: .24em;
  text-transform: uppercase; color: var(--page-text-3);
}
.cta-foot::before, .cta-foot::after { content: ""; flex: 1; height: 1px; background: var(--page-line); }
@media (max-width: 1180px) { .cta-note { display: none; } }
@media (max-width: 760px) {
  .cta .wrap { padding-top: 96px; padding-bottom: 56px; }
  .cta-points { flex-direction: column; align-items: flex-start; width: fit-content; gap: var(--s-5); margin-top: 40px; }
  .cta-points li { padding: 0; }
  .cta-points li + li { border-left: 0; }
  .cta-btns .btn { flex: 1 1 100%; }
  .cta-checks { flex-direction: column; align-items: center; }
  .cta .wrap.cta-foot { letter-spacing: .16em; gap: var(--s-4); }
}

/* ---------------- Case-study modal ----------------
   Four cases live in the markup inside one overlay; app.js only decides which
   article is not [hidden]. Modelled on the reference's .cs-overlay: a bar with
   a close control, then the case as two columns - prose and spec on the left,
   the visuals on the right. */
/* Full-bleed and dark, on the reference's proportions: the overlay IS the
   surface rather than a panel floating on a dimmed page, and it owns the
   scroll. Measured off the reference at 1440: a 1280 column, a sticky bar with
   a pill close, and a 1 : 1.25 split with a 72px gutter. The reference is dark;
   this is not — it takes the structure and the site's own light palette, the
   same way the rest of the page borrows from it. */
.pm {
  position: fixed; inset: 0; z-index: 200;
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  background: var(--bg); color: var(--ink);
  animation: motion-fade .3s var(--ease-out) both;
}
.pm[hidden] { display: none; }
/* Transparent now that the overlay carries the colour, and FIXED rather than
   absolute so it stays a click target down the whole scroll instead of only
   the first screenful. */
.pm-backdrop { position: fixed; inset: 0; z-index: 0; background: none; }
.pm-panel {
  position: relative; z-index: 1;
  width: 100%; max-width: 1280px; margin: 0 auto;
  padding: 0 clamp(20px, 4.5vw, 65px) var(--s-30);
  animation: pmRise .45s var(--ease-reveal) both;
}
.pm-panel:focus { outline: none; }
@keyframes pmRise { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }

.pm-bar {
  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); padding: 20px 0; background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.pm-bar-label { font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: var(--ls-mono); text-transform: uppercase; color: var(--ink-4); }
.pm-close {
  display: inline-flex; align-items: center; gap: 9px; flex: none;
  padding: 11px 20px; border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--bg); color: var(--ink); cursor: pointer;
  font-family: var(--font-mono); font-size: var(--t-s); letter-spacing: var(--ls-mono); text-transform: uppercase;
  transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.pm-close:hover { background: var(--ink); border-color: var(--ink); color: #fff; }

/* The overlay scrolls, not an inner box — so this is a plain block. */
.pm-scroll { display: block; }
.pm-case[hidden] { display: none; }

.pm-head { padding: var(--s-10) 0; border-bottom: 1px solid var(--line); }
.pm-ix { font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: var(--ls-mono); text-transform: uppercase; color: var(--accent); }
.pm-head h2 { margin-top: var(--s-4); font-family: var(--font-display); font-size: var(--t-h1); font-weight: 700; letter-spacing: -0.03em; line-height: 1.02; color: var(--ink); }
.pm-lede { margin-top: var(--s-5); max-width: 62ch; font-size: var(--t-lede); line-height: 1.6; color: var(--ink-2); }
.pm-facts { margin-top: var(--s-6); display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3); font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: var(--ls-mono); text-transform: uppercase; color: var(--ink-4); }
.pm-facts i { font-style: normal; opacity: .6; }

.pm-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 72px; padding-top: var(--s-16); align-items: start; }
.pm-block + .pm-block { margin-top: var(--s-10); }
.pm-block h3 { font-size: var(--t-h4); font-weight: 700; letter-spacing: -0.018em; color: var(--ink); }
.pm-block p { margin-top: var(--s-3); font-size: var(--t-lg); line-height: 1.68; color: var(--ink-2); }
.pm-spec { margin-top: var(--s-12); }
.pm-cta { margin-top: var(--s-10); align-self: flex-start; }

/* The spec card, the stack list and the button come straight from the
   case-study pages and need no overrides here — same palette, same tokens. */

/* A slot rather than a bare <img>: a case can swap its stills for a video
   later without any of this changing. */
.pm-media { display: grid; gap: var(--s-6); }
.pm-media figure { margin: 0; }
.pm-media img { width: 100%; height: auto; display: block; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--bg-muted); }
.pm-media figcaption { margin-top: var(--s-3); font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-4); }
/* Toyota has no gallery - one shot is all that exists, so it is given room to
   be the whole column rather than sitting small at the top of an empty grid. */
.pm-media--single { align-content: start; }

/* The overlay owns the scroll while it is open; the page behind it must not
   move. That is html.scroll-locked (styles.css), set by the observer in app.js
   for this class and menu-open alike. A body-level overflow here did nothing:
   html is overflow-x: clip, so body overflow never reaches the viewport. */

@media (max-width: 960px) {
  .pm-cols { grid-template-columns: minmax(0, 1fr); gap: var(--s-10); }
  .pm-spec { margin-top: var(--s-8); }
  /* One column: the pictures come first, right under the title. Below the
     text they sat after the stack card and the button, a long way down, and
     read as "no image" (owner, 2026-09-24). The first shot full width, any
     others two to a row, so they don't push the text a screen further. */
  .pm-media { order: -1; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
  .pm-media figure:first-child { grid-column: 1 / -1; }
  .pm-media figure:not(:first-child) figcaption { display: none; }
}
@media (max-width: 660px) {
  .pm-head { padding: var(--s-8) 0 var(--s-7); }
  .pm-cols { padding-top: var(--s-8); }
}

/* ---------------- Footer ---------------- */
.footer { background: var(--d-bg); color: var(--d-ink-2); border-top: 1px solid var(--d-line-soft); padding: 72px 0 36px; }
.foot-grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: 40px; }
.foot-brand .brand { color: #fff; margin-bottom: 18px; width: fit-content; }
.foot-brand p { font-size: var(--t-sm); max-width: 300px; line-height: 1.6; }
.foot-soc { display: flex; gap: 10px; margin-top: 22px; }
.foot-soc a { width: 38px; height: 38px; border: 1px solid var(--d-line); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--d-ink-2); transition: border-color .2s, color .2s, transform .2s; }
.foot-soc a:hover { color: #fff; border-color: rgba(255,255,255,.4); transform: translateY(-2px); }
.foot-col h4 { font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: .1em; text-transform: uppercase; color: var(--d-ink-3); font-weight: 500; margin-bottom: 16px; }
.foot-col a { display: block; font-size: var(--t-sm); padding: 6px 0; color: var(--d-ink-2); transition: color .2s; }
.foot-col a:hover { color: #fff; }
.foot-bottom { margin-top: 56px; padding-top: 24px; border-top: 1px solid var(--d-line-soft); display: flex; align-items: center; justify-content: space-between; gap: 16px; font-size: var(--t-s); color: var(--d-ink-3); font-family: var(--font-mono); letter-spacing: .03em; flex-wrap: wrap; }
/* The bottom right used to read "Tirana · Remote", a second copy of the
   Contact column's address. It is a way back up instead, as on atrax.al. */
.foot-top { color: var(--d-ink-3); transition: color .2s; }
.foot-top:hover { color: #fff; }

/* ---------------- Responsive ---------------- */
@media (max-width: 1080px) {
  .nav-inner { padding-left: 18px; padding-right: 18px; }
  .nav-links a { padding-left: 8px; padding-right: 8px; }
  .svc-layout { grid-template-columns: 1fr; gap: 36px; }
  .svc-aside { position: static; }
  .eng-grid { grid-template-columns: 1fr 1fr; }
  /* two-up: the row keeps its frame, the hairlines just move */
  .wwd-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wwd:nth-child(2n+1) { border-left: 0; }
  .wwd:nth-child(n+3) { border-top: 1px solid var(--line); }
}
@media (max-width: 860px) {
  /* Book a call stays in the bar at every width (2026-09-24, owner: "the
     book now button should be visible the whole time on mobile"). The bar is
     fixed, so it is on screen from the first pixel of every page. */
  .nav-links, .nav-cta.desk { display: none; }
  .nav-right { gap: 6px; }
  .nav-cta-link { padding: 0 16px; }
  .nav-burger { display: block; }
  .mobile-menu { display: block; }
  /* No backdrop blur on phones. It re-samples everything under the bar on
     every scrolled frame and re-rasterises whenever the theme tween moves,
     which is the heaviest single paint on a phone scroll. The bar takes the
     page's own ground instead: opaque, and the same colour it was blurring. */
  .nav { backdrop-filter: none; -webkit-backdrop-filter: none; background: var(--page-bg); }
  .mobile-menu { backdrop-filter: none; -webkit-backdrop-filter: none; background: var(--page-bg); }
  /* Logo, burger and menu links on the 32px content line (bar 16px in, 1px
     border, 15px inside); the 660 block below re-derives the same for 20px. */
  .nav-inner { padding-left: 15px; padding-right: 15px; }
  .mobile-menu { padding-left: 15px; padding-right: 15px; }
  .mobile-menu a { padding-left: 0; padding-right: 0; }
  .section { padding: 84px 0; }
  /* 2-up: keep the rules, re-draw them for the new cell positions */
  .stats-grid { grid-template-columns: 1fr 1fr; gap: 0; }
  .stat { padding: var(--s-6) var(--s-6) var(--s-6) 0; }
  .stat:nth-child(2n) { border-right: 0; padding-right: 0; }
  .stat:nth-child(2n+1) { padding-left: 0; }
  .stat:nth-child(n+3) { border-top: 1px solid var(--line); }
  /* five cells over two columns leaves the last one alone on its row, keeping a
     border-right that now points at nothing */
  .stat:last-child { border-right: 0; }
  /* one column: every card takes its own ratio back, since there is no longer
     a right-hand column for the tall one to match */
  .work { grid-template-columns: minmax(0, 1fr); gap: 48px; }
  .wk-a, .wk-b, .wk-c, .wk-d, .wk-e, .wk-f, .wk-wide { grid-column: 1; grid-row: auto; }
  /* Keeps 3:4 rather than dropping to 4:5: the card holds two stacked
     landscape shots, and the shorter frame squeezed them. */
  .wk-wide .work-thumb { aspect-ratio: 4 / 3; }
  .foot-grid { grid-template-columns: 1fr 1fr; gap: 32px; }
  .foot-brand { grid-column: 1 / -1; }
}
@media (max-width: 660px) {
  :root { --gutter: 20px; }

  /* A bigger headline on a phone, and this is very nearly all there is to take.
     The three lines are hard-coded spans, so the size is capped by the longest
     of them fitting on one line: "Your Engineering Partner" measures 10.96em at
     the desktop tracking, which at a 40px gutter tops out at 7.98vw across
     320-430px. Tightening the tracking a step - which a display size wants
     anyway - buys 10.72em and lifts that to 8.16vw, so 8vw carries a 2% margin
     against a wrap at every phone width.

     27px to 30px is the whole envelope. Going meaningfully bigger than this
     means re-breaking the headline into four shorter lines on phones, which
     cannot be done in CSS alone while the desktop keeps three. */
  /* 7.4vw, not 8vw (2026-09-23): 8vw left a 2% margin, and the caret plus a
     fallback font during load were enough to wrap a line mid-typing. */
  .hero h1 { font-size: clamp(24px, 7.4vw, 95px); letter-spacing: -0.055em; }
  /* The bar sits 8px in and its contents 11px inside that (+1px border), so the
     logo and the burger land on the same 20px line as everything below them.
     They sat at 29px and 25px, out of line with the page. */
  .nav { top: 12px; width: calc(100% - 16px); }
  .nav-inner { height: 56px; padding-left: 11px; padding-right: 11px; }
  .mobile-menu { top: 80px; width: calc(100% - 16px); padding-left: 11px; padding-right: 11px; }
  .mobile-menu a { padding-left: 0; padding-right: 0; }
  .section { padding: 64px 0; }
  .section.clients { padding: 52px 0; }
  .section-head p { font-size: var(--t-lg); }
  .eng-grid { grid-template-columns: 1fr; }
  .work-thumb .scene { padding: 22px; }
  .wwd-row { grid-template-columns: minmax(0, 1fr); }
  .wwd { border-left: 0; padding: var(--s-7) var(--s-6) var(--s-8); }
  .wwd + .wwd { border-top: 1px solid var(--line); }

  /* Card meta on two lines: title and year share the first, the descriptor
     takes the second. The desktop rule gives the descriptor a 320px flex
     basis, which at this width cannot sit beside anything — so it claimed its
     own row and left the year stranded, right-aligned, on a third. */
  .work-name { flex: 0 1 auto; }   /* must not grow, or it pushes the year off the line */
  .work-year { order: 1; margin-left: auto; }
  .work-desc { order: 2; flex: 1 1 100%; }
}

/* ============================================================
   Theme transparency — sections let the global theme-canvas
   show through, so the whole viewport eases light <-> dark.
   ============================================================ */
.hero, .clients, .services, .projects, .stats,
.reach, .engage, .testi, .cta {
  background: transparent;
}
/* dotted texture now lives on the global canvas (opacity = --theme-t)

   The footer is NOT in that list any more (2026-09-23, owner: "the footer
   has those dots background"). Transparent, it showed the global dot
   texture through. It keeps its own solid --d-bg instead, which is the
   same #0e0e11 as the dark plate, and the page is fully dark by the time
   the footer is on screen, so there is no seam - just no dots. */

/* ============================================================
   Industries + Services grid (2026-09-23). Homepage shows a few of
   each above "Four ways"; services.html#industries / #expertise
   carry the full sets with the same components.
   ============================================================ */
.inds, .caps { background: transparent; }
/* Industries: atrax.al's old block (icon, name, three ticks), redrawn in
   this site's idiom: the .ai-alt-icon tile, a check per line, hairline
   rows rather than boxes. */
.ind-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: clamp(var(--s-6), 3vw, var(--s-12));
  border-top: 1px solid var(--page-line);
}
.ind {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s-5);
  padding: var(--s-8) 0; border-bottom: 1px solid var(--page-line);
}
.ind-ico {
  width: 48px; height: 48px; display: grid; place-items: center;
  border-radius: var(--r-lg); background: var(--accent-soft); color: var(--accent);
}
.ind-ico svg { width: 24px; height: 24px; display: block; }
.ind h3 {
  font-family: var(--font-display); font-size: var(--t-xl); font-weight: 700;
  letter-spacing: -0.02em; line-height: 1.2; color: var(--page-text);
}
.ind-list { list-style: none; margin: var(--s-4) 0 0; padding: 0; display: grid; gap: var(--s-2); }
.ind-list li {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s-3); align-items: start;
  font-size: var(--t-sm); line-height: 1.5; color: var(--page-text-2);
}
.ind-list svg { width: 15px; height: 15px; margin-top: 3px; color: var(--page-text-3); }
.ind-more { margin-top: var(--s-12); display: flex; justify-content: center; }
/* Each industry links to the portfolio filtered to it (reviewer, 2026-09-23);
   the services tiles do the same through .cap-proj, which is .cap-go. */
.ind-go {
  display: inline-flex; align-items: center; gap: var(--s-2); margin-top: var(--s-4);
  font-family: var(--font-display); font-size: var(--t-sm); font-weight: 600; color: var(--accent);
}
.ind-go .arr, .cap-proj .arr { transition: transform .25s var(--ease); }
.ind-go:hover .arr, .cap-proj:hover .arr { transform: translateX(3px); }
.cap-proj { position: relative; z-index: 1; }

/* Services: joined tiles after the reviewer's reference. The frame and hover
   are .wwd-row's, so the three stacked sections read as one family. Borders
   are the grid's own 1px gap over a --page-line ground, so tiles share edges
   at any column count without per-child border rules. */
.cap-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px;
  background: var(--page-line); border: 1px solid var(--page-line);
  border-radius: var(--r-lg); overflow: hidden;
}
.caps--four .cap-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.cap {
  position: relative; display: flex; flex-direction: column;
  padding: var(--s-10) var(--s-8) var(--s-12); background: var(--page-bg);
  color: inherit; text-decoration: none; transition: background .4s var(--ease);
}
.cap:hover { background: var(--page-surface); }
.cap-ico { color: var(--accent); }
.cap-ico svg { width: 32px; height: 32px; display: block; }
.cap h3 {
  margin-top: var(--s-6); font-family: var(--font-display); font-size: var(--t-xl);
  font-weight: 700; letter-spacing: -0.02em; line-height: 1.15; color: var(--page-text);
}
.cap p { margin-top: var(--s-4); max-width: 42ch; font-size: var(--t-sm); line-height: 1.55; color: var(--page-text-2); }
/* The reference's "Learn more" strip: the only accent fill in the grid, and
   only on the tile that has somewhere further to go. */
.cap--strip { padding-bottom: calc(var(--s-12) + 44px); }
.cap-strip {
  position: absolute; left: 0; right: 0; bottom: 0; height: 44px;
  display: flex; align-items: center; gap: var(--s-2); padding: 0 var(--s-8);
  background: var(--accent-soft); color: var(--accent);
  font-family: var(--font-display); font-size: var(--t-sm); font-weight: 600;
  transition: background .3s var(--ease), color .3s var(--ease);
}
.cap-strip:hover { background: var(--accent); color: #fff; }
.cap-strip .arr, .cap-go .arr { transition: transform .25s var(--ease); }
.cap-strip:hover .arr, .cap-all:hover .cap-go .arr { transform: translateX(3px); }
.cap-go {
  margin-top: auto; padding-top: var(--s-6); display: inline-flex; align-items: center; gap: var(--s-2);
  font-family: var(--font-display); font-size: var(--t-sm); font-weight: 600; color: var(--accent);
}
.cap-all:hover h3 { color: var(--accent); }

@media (max-width: 1080px) {
  .caps--four .cap-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 860px) {
  .ind-grid, .cap-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 660px) {
  .ind-grid, .cap-grid, .caps--four .cap-grid { grid-template-columns: minmax(0, 1fr); }
  .cap { padding: var(--s-8) var(--s-6) var(--s-10); }
  .cap--strip { padding-bottom: calc(var(--s-10) + 44px); }
  .cap-strip { padding: 0 var(--s-6); }
  .ind-ico { width: 44px; height: 44px; }
}
