/* ==========================================================================
   AstroPanda, global.css
   Site-wide styles: variables, reset/base, typography, shared buttons,
   header, footer and other reusable elements. Anything specific to the
   home page's own sections (hero, panda rail, services, success stories,
   testimonials) lives in home.css instead.
   ========================================================================== */

:root{
  --ink:#030305;
  --paper:#eef0f6;
  --orange:#ff5a14;
  --orange-soft:#ff8a3d;
  --orange-supersoft:#ff8b3dc7;
  --muted:rgba(238,240,246,0.62);
  --line:rgba(238,240,246,0.12);
  --shell:1080px;
  --panda-w:400px;
  --panda-h:563px;   /* 400 x 619/440 */
}

*{box-sizing:border-box}
/* clip (not hidden) → keeps the viewport as the scroll container and doesn't break position:sticky.
   No scroll-behavior:smooth and no native snap here: section travel is animated
   by hand in main.js (easeInOutCubic), native snap can't be timed or eased. */
html{overflow-x:clip}
body{
  margin:0; padding:0;
  background:var(--ink);
  color:var(--paper);
  font-family:'Space Grotesk',sans-serif;
  -webkit-font-smoothing:antialiased;
}
a{color:var(--paper); text-decoration:none}
a:hover{color:var(--orange)}
img{display:block; max-width:100%}

/* ---------- background layers ---------- */
#stars{position:fixed; inset:0; z-index:0; pointer-events:none}
.ap-glow{
  /* no blur filter: these are radial gradients, already soft. Blurring a 52vw
     surface every frame was pure waste. */
  position:fixed; z-index:0; pointer-events:none; border-radius:50%;
  opacity:.5;
}
.ap-glow--a{top:-10vh; left:-8vw; width:52vw; height:52vw;
  background:radial-gradient(circle,rgba(255,90,20,.17) 0%,rgba(255,90,20,.06) 42%,rgba(255,90,20,0) 76%)}
.ap-glow--b{bottom:-18vh; right:-12vw; width:58vw; height:58vw;
  background:radial-gradient(circle,rgba(90,120,255,.14) 0%,rgba(90,120,255,.05) 42%,rgba(90,120,255,0) 76%)}

.ap-planet{position:absolute; z-index:1; pointer-events:none; will-change:transform}
@keyframes apDriftL{0%,100%{transform:translate(0,0)}50%{transform:translate(6px,-14px)}}
@keyframes apDriftR{0%,100%{transform:translate(0,0)}50%{transform:translate(-10px,12px)}}

/* ---------- shell / header ---------- */
.ap-shell{width:min(var(--shell),100% - 88px); margin-inline:auto; position:relative; z-index:2}

/* Decorative plates on the home and the service rows are composed to run
   off the viewport edges. main is exactly viewport-wide, so this clips
   what hangs over without moving anything. `clip`, not `hidden`: hidden
   would make main a scroll container and break the panda rail's sticky. */
main{overflow-x:clip}

@keyframes apCosmic{
  0%{background-position:0% 50%}
  50%{background-position:100% 50%}
  100%{background-position:0% 50%}
}
.ap-header{
  position:fixed; top:0; left:0; right:0; z-index:50;
  /* while hidden it must be fully inert AND invisible: its blur/pool pseudo-layers
     extend past the translated box and were shading the hero's own logo and CTA */
  pointer-events:none; opacity:0; visibility:hidden;
  transform:translateY(-100%);
  transition:transform .45s cubic-bezier(.22,.61,.36,1), opacity .35s, visibility 0s .45s;
  display:grid; grid-template-columns:1fr auto 1fr; align-items:center;
  padding:22px 44px 26px;
  background:transparent;
}
/* Cosmic wash behind the bar: an ever-moving gradient over a blurred backdrop,
   dissolved at its lower edge by a mask instead of ending on a hard line. */
/* Two layers, both bleeding past the bar so nothing ever shows a box edge:
   ::before = a soft blur veil across the whole top, dissolved by a tall mask;
   ::after  = three dark pools (logo / links / CTA) that drift slowly. */
.ap-header::before{
  content:''; position:absolute; inset:-24px 0 -64px 0; z-index:-2;
  /* it reaches 64px below the bar so the blur can dissolve: without this
     that strip swallows clicks on everything underneath it */
  pointer-events:none;
  backdrop-filter:blur(15px) saturate(1.1);
  -webkit-backdrop-filter:blur(15px) saturate(1.1);
  mask-image:linear-gradient(180deg,#000 0%,#000 34%,rgba(0,0,0,.45) 66%,transparent 96%);
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 34%,rgba(0,0,0,.45) 66%,transparent 96%);
}
/* Moves the whole layer rather than its background-position: a transform is
   composited, while background-position repainted this masked, page-wide
   layer on every frame, on every page, for as long as it was open. */
@keyframes apPools{
  0%,100%{transform:translate3d(0,0,0)}
  33%{transform:translate3d(28px,6px,0)}
  66%{transform:translate3d(-24px,-6px,0)}
}
.ap-header::after{
  content:''; position:absolute; inset:-40px -120px -90px -120px; z-index:-1; pointer-events:none;
  background:
    radial-gradient(ellipse 480px 170px at 11% 42%, rgba(5,8,20,.92) 0%, rgba(9,10,26,.45) 55%, transparent 78%),
    radial-gradient(ellipse 700px 190px at 50% 38%, rgba(6,8,22,.9) 0%, rgba(14,10,30,.4) 58%, transparent 80%),
    radial-gradient(ellipse 460px 170px at 89% 42%, rgba(5,8,20,.92) 0%, rgba(9,10,26,.45) 55%, transparent 78%);
  background-repeat:no-repeat;
  animation:apPools 26s ease-in-out infinite; will-change:transform;
  mask-image:linear-gradient(180deg,#000 0%,#000 50%,transparent 94%);
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 50%,transparent 94%);
}
/* the exported hero already carries its own nav, so ours only appears past it */
.ap-header.is-visible{transform:none; pointer-events:auto; opacity:1; visibility:visible; transition:transform .45s cubic-bezier(.22,.61,.36,1), opacity .35s, visibility 0s}
.ap-logo{font-weight:700; font-size:17px; line-height:1.02; letter-spacing:.02em}
.ap-logo span{color:var(--orange)}
.ap-nav{display:flex; align-items:center; gap:32px; justify-self:center;
  font-size:14.5px; letter-spacing:.12em; font-weight:700}
.ap-nav a.is-current{color:var(--orange)}

/* nav dropdown (desktop), hover or keyboard-focus reveals it; the trigger
   link keeps working as a normal link to the services hub */
.ap-nav-drop{position:relative; display:inline-flex; align-items:center}
.ap-nav-dropdown{
  position:absolute; top:100%; left:50%; z-index:10;
  min-width:230px; margin-top:22px; padding:10px;
  border-radius:12px; border:1px solid var(--orange-supersoft);
  background:rgba(10,13,20,.27); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  box-shadow:0 22px 50px rgba(0,0,0,.5);
  display:flex; flex-direction:column; gap:2px; text-align:left;
  opacity:0; visibility:hidden; pointer-events:none;
  transform:translate(-50%,4px);
  transition:opacity .2s ease, transform .2s ease, visibility 0s .2s;
}
.ap-nav-drop::after{
  content:'';
  position:absolute;
  left:-30px;
  right:-30px;
  top:100%;
  height:34px;   /* bridges the pointer across the gap down to the mega panel */
}
.ap-nav-drop:hover .ap-nav-dropdown,
.ap-nav-drop:focus-within .ap-nav-dropdown{
  opacity:1; visibility:visible; pointer-events:auto; transform:translate(-50%,0);
  transition:opacity .2s ease, transform .2s ease, visibility 0s;
}

/* ---------- services mega menu ----------
   Replaces the small dropdown for nav items that carry children with imagery.
   The panel is position:fixed so it can span the viewport regardless of where
   the trigger sits in the nav; it still opens on the trigger's :hover /
   :focus-within exactly like the old dropdown did. */
.ap-mega{
  position:fixed; left:50%; top:76px; z-index:20;
  width:min(1620px, calc(100vw - 48px));
  transform:translate(-50%,6px);
  border-radius:18px; border:1px solid var(--line);
  background:rgba(8,10,18,.93);
  backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
  box-shadow:0 30px 70px rgba(0,0,0,.6);
  padding:26px 30px 20px;
  text-align:left;
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .22s ease, transform .22s ease, visibility 0s .22s;
}
/* Darkens everything under the open panel. Starts below the navbar row on
   purpose, and stays pointer-events:none so it neither blocks the page nor
   interferes with the hover that keeps the menu open. Lives inside the
   header's stacking context, under the panel's z-index. */
.ap-mega-veil{
  position:fixed; left:0; right:0; top:76px; bottom:0; z-index:10;
  background:rgba(3,4,9,.72);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .25s ease, visibility 0s .25s;
}
.ap-nav-drop:hover .ap-mega-veil,
.ap-nav-drop:focus-within .ap-mega-veil{
  opacity:1; visibility:visible;
  transition:opacity .25s ease, visibility 0s;
}
.ap-nav-drop:hover .ap-mega,
.ap-nav-drop:focus-within .ap-mega{
  opacity:1; visibility:visible; pointer-events:auto;
  transform:translate(-50%,0);
  transition:opacity .22s ease, transform .22s ease, visibility 0s;
}
.ap-mega-head{
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:10px 2px 18px;
}
.ap-mega-eyebrow{
  font-size:11px; font-weight:700; letter-spacing:.34em; color:rgba(238,240,246,.55);
}
.ap-mega-all{
  display:inline-flex; align-items:center; gap:8px;
  font-size:12px; font-weight:700; letter-spacing:.2em; color:var(--paper);
}
.ap-mega-all i{font-style:normal; font-size:16px; color:var(--orange)}
.ap-mega-all:hover{color:var(--orange)}
.ap-mega-grid{
  /* --cols comes from how many children the nav item has, so adding a
     service is a data change and not a CSS one. */
  display:grid; grid-template-columns:repeat(var(--cols,5),1fr); gap:14px;
}
/* Service rows reveal artwork and copy, matching the industry pages. */
.ap-mega--services{width:min(1300px,calc(100vw - 48px));max-height:calc(100dvh - 100px);overflow-y:auto;background:#080b12;padding:22px 28px 28px}
.ap-ms-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:40px}
.ap-ms-column{border-top:1px solid var(--line)}
.ap-ms-row{display:block;padding:24px 12px;border-bottom:1px solid var(--line);color:var(--paper);text-decoration:none;transition:background .25s ease}
.ap-ms-heading{display:flex;align-items:center;justify-content:space-between;gap:20px}
.ap-ms-title{font-family:'Anton',sans-serif;font-size:clamp(20px,1.75vw,27px);font-weight:400;letter-spacing:0;line-height:1.2;text-transform:none}
.ap-ms-btn{display:inline-flex;align-items:center;gap:10px;flex:none;padding:10px 14px;border:1px solid var(--line);border-radius:8px;background:rgba(238,240,246,.035);font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;white-space:nowrap;transition:background .25s ease,border-color .25s ease}
.ap-ms-btn i{font-style:normal;color:var(--orange)}
.ap-ms-panel{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .4s cubic-bezier(.22,.61,.36,1),opacity .25s ease}
.ap-ms-detail{display:grid;grid-template-columns:160px minmax(0,1fr);align-items:center;gap:22px;overflow:hidden;min-height:0}
.ap-ms-detail > *{margin-top:20px}
.ap-ms-art{display:block;overflow:hidden;border-radius:10px;border:1px solid var(--line)}
.ap-ms-art img{display:block;width:100%;height:auto;aspect-ratio:16/10;object-fit:cover}
.ap-ms-desc{font-size:14px;line-height:1.6;letter-spacing:normal;font-weight:400;color:var(--muted)}
.ap-ms-row:hover,.ap-ms-row:focus-visible{background:rgba(238,240,246,.03)}
.ap-ms-row:focus-visible{outline:1px solid var(--orange);outline-offset:-1px}
.ap-ms-row:hover .ap-ms-panel,.ap-ms-row:focus-visible .ap-ms-panel{grid-template-rows:1fr;opacity:1}
.ap-ms-row:hover .ap-ms-btn,.ap-ms-row:focus-visible .ap-ms-btn{background:rgba(255,90,20,.12);border-color:var(--orange)}
@media(max-width:1180px){.ap-ms-grid{gap:24px}.ap-ms-row{padding:22px 8px}.ap-ms-heading{gap:12px}.ap-ms-btn{padding:10px;font-size:9px}.ap-ms-detail{grid-template-columns:130px minmax(0,1fr);gap:16px}}
@media(hover:none){.ap-ms-panel{grid-template-rows:1fr;opacity:1}}
@media(prefers-reduced-motion:reduce){.ap-ms-panel,.ap-ms-row,.ap-ms-btn{transition:none}}
/* INDUSTRIES panel: a list, not a wall of cards.
   Seven image cards in one row had already pushed the title to 22px so
   "CONSTRUCTION & REAL ESTATE" would fit a seventh of the width, and the
   sector list only grows. Two columns of links hold any number of them at
   a readable size, and the panel keeps exactly one picture: the feature
   card on the right, which is also the way out of the list. */
.ap-mega--ind{background:#080b12;padding:28px 30px 30px}
.ap-mega--ind .ap-mega-head{padding:4px 8px 22px}
.ap-mi-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,1.02fr);gap:28px;align-items:stretch}
.ap-mi-list{grid-column:1/3;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:1fr;gap:0 28px;align-content:stretch}
.ap-mi-item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:18px 8px;text-decoration:none;color:inherit;transition:background .2s ease}
.ap-mi-item:nth-child(even){border-left:1px solid rgba(238,240,246,.08);padding-left:28px}
.ap-mi-item:hover,.ap-mi-item:focus-visible{background:rgba(238,240,246,.035)}
.ap-mi-text{display:flex;flex-direction:column;gap:7px;min-width:0}
.ap-mi-text strong{font-size:16px;font-weight:600;letter-spacing:0;line-height:1.3;color:var(--paper);text-wrap:balance}
.ap-mi-text span{font-size:14px;font-weight:400;letter-spacing:.01em;line-height:1.55;color:#a4a7b0;text-wrap:pretty}
.ap-mi-arrow{flex:none;display:inline-flex;font-size:13px;color:var(--orange);opacity:0;translate:-4px 0;transition:opacity .2s ease,translate .2s ease}
.ap-mi-item:hover .ap-mi-arrow,.ap-mi-item:focus-visible .ap-mi-arrow{opacity:1;translate:0 0}
.ap-mi-feature{position:relative;display:flex;flex-direction:column;overflow:hidden;border-radius:14px;border:1px solid rgba(238,240,246,.14);background:#090c12;text-decoration:none;color:inherit;transition:border-color .2s ease}
.ap-mi-feature:hover{border-color:rgba(255,90,20,.45)}
.ap-mi-feature-art{position:relative;display:block;height:150px;overflow:hidden;flex:none}
.ap-mi-feature-art img{width:100%;height:100%;object-fit:cover;object-position:center 48%;display:block}
.ap-mi-feature-body{display:flex;flex-direction:column;align-items:flex-start;gap:12px;padding:20px 24px 24px}
.ap-mi-feature-eyebrow{font-family:'Space Mono',monospace;font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--orange)}
.ap-mi-feature-title{font-family:'Anton',sans-serif;font-weight:400;text-transform:uppercase;font-size:clamp(24px,1.95vw,34px);line-height:1.1;letter-spacing:.005em;color:var(--paper)}
.ap-mi-feature-desc{font-size:14px;font-weight:400;letter-spacing:.025em;line-height:1.5;color:#a4a7b0}
.ap-mi-feature-btn{margin-top:10px;width:100%;justify-content:center;min-height:50px;padding:12px 16px;border:1px solid var(--orange);background:transparent;pointer-events:none;font-size:11px;letter-spacing:.13em;white-space:normal;text-align:center}
@media(max-width:1180px){
  .ap-mega--ind{padding:20px}
  .ap-mi-grid{gap:18px;grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,1.1fr)}
  .ap-mi-list{gap:0 18px}
  .ap-mi-item:nth-child(even){padding-left:18px}
  .ap-mi-text strong{font-size:14px}
  .ap-mi-text span{font-size:12px}
  .ap-mi-feature-body{padding:18px}
  .ap-mi-feature-art{height:130px}
}
/* Cards rest dark with the label centred, and reveal their artwork on hover
   while the text slides down to sit over it. Fixed aspect so "centred" means
   something: with the image out of the flow the card has no intrinsic
   height. */
.ap-mega-card{
  position:relative; display:block;
  container-type:inline-size;
  aspect-ratio:6/7;
  overflow:hidden;
  border-radius:14px; border:1px solid rgba(238,240,246,.1);
  background:rgba(9,11,17,.92);
  transition:border-color .25s ease, box-shadow .25s ease, transform .25s ease;
}
.ap-mega-card:hover{
  border-color:rgba(255,110,45,.85);
  box-shadow:0 0 26px rgba(255,90,20,.18);
  transform:translateY(-2px);
}
/* The resting card is a patch of sky rather than an empty panel: a nebula
   wash, a soft band across it and two layers of stars painted as radial
   gradients, the near one drifting and breathing. No images and no canvas,
   so five of these cost nothing while the menu is closed. --i (the card's
   position) shifts every clock, so the row never pulses in unison. */
.ap-mega-stars{
  position:absolute; inset:0; display:block; overflow:hidden;
  opacity:1; transition:opacity .4s ease;
  /* Dim on purpose: the card is a piece of sky, not a lit surface, and the
     tint only has to keep it from reading as flat black. Each card gets the
     wash from a different corner (see below) so the row is five patches of
     the same sky rather than five copies of one. */
  background:
    radial-gradient(78% 56% at 74% 6%, rgba(255,90,20,.09), transparent 70%),
    radial-gradient(66% 48% at 6% 96%, rgba(120,150,255,.06), transparent 72%),
    linear-gradient(203deg, transparent 36%, rgba(150,170,255,.035) 50%, transparent 64%),
    #090b11;
}
/* One wash per card. Same two hues and the same low strengths throughout;
   what changes is where the light comes from and which way the band runs. */
.ap-mega-card:nth-child(2) .ap-mega-stars{
  background:
    radial-gradient(74% 54% at 12% 10%, rgba(255,90,20,.085), transparent 70%),
    radial-gradient(70% 52% at 92% 88%, rgba(120,150,255,.055), transparent 72%),
    linear-gradient(158deg, transparent 38%, rgba(150,170,255,.03) 52%, transparent 66%),
    #090b11;
}
.ap-mega-card:nth-child(3) .ap-mega-stars{
  background:
    radial-gradient(82% 50% at 50% 104%, rgba(255,90,20,.08), transparent 68%),
    radial-gradient(60% 46% at 16% 4%, rgba(120,150,255,.06), transparent 74%),
    linear-gradient(22deg, transparent 34%, rgba(150,170,255,.032) 50%, transparent 66%),
    #090b11;
}
.ap-mega-card:nth-child(4) .ap-mega-stars{
  background:
    radial-gradient(70% 58% at 96% 44%, rgba(255,90,20,.075), transparent 70%),
    radial-gradient(64% 50% at 2% 62%, rgba(120,150,255,.06), transparent 72%),
    linear-gradient(240deg, transparent 36%, rgba(150,170,255,.03) 52%, transparent 68%),
    #090b11;
}
.ap-mega-card:nth-child(5) .ap-mega-stars{
  background:
    radial-gradient(76% 52% at 28% 96%, rgba(255,90,20,.08), transparent 70%),
    radial-gradient(68% 50% at 86% 12%, rgba(120,150,255,.055), transparent 72%),
    linear-gradient(186deg, transparent 38%, rgba(150,170,255,.028) 50%, transparent 64%),
    #090b11;
}
.ap-mega-stars::before,.ap-mega-stars::after{
  content:""; position:absolute; inset:-14%;
  background-repeat:no-repeat;
}
/* the near field: bright, with a couple of haloed stars, and it moves */
.ap-mega-stars::before{
  background-image:
    radial-gradient(9px 9px at 46% 57%, rgba(255,180,130,.16), transparent 70%),
    radial-gradient(8px 8px at 18% 21%, rgba(200,220,255,.13), transparent 70%),
    radial-gradient(2.2px 2.2px at 18% 21%, #fff, transparent),
    radial-gradient(2px 2px at 74% 13%, rgba(255,255,255,.95), transparent),
    radial-gradient(2.4px 2.4px at 46% 57%, rgba(255,226,206,1), transparent),
    radial-gradient(1.9px 1.9px at 86% 47%, rgba(255,255,255,.9), transparent),
    radial-gradient(2.1px 2.1px at 11% 73%, rgba(255,255,255,.88), transparent),
    radial-gradient(1.8px 1.8px at 62% 81%, rgba(255,196,150,.92), transparent),
    radial-gradient(2px 2px at 30% 39%, rgba(255,255,255,.9), transparent),
    radial-gradient(2.3px 2.3px at 88% 77%, rgba(255,255,255,.92), transparent),
    radial-gradient(1.9px 1.9px at 54% 6%, rgba(205,220,255,.9), transparent),
    radial-gradient(2px 2px at 6% 33%, rgba(255,255,255,.85), transparent);
}
/* the far field: dimmer and slower, and it drifts the other way */
.ap-mega-stars::after{
  background-image:
    radial-gradient(1.4px 1.4px at 32% 11%, rgba(255,255,255,.7), transparent),
    radial-gradient(1.3px 1.3px at 58% 29%, rgba(255,255,255,.62), transparent),
    radial-gradient(1.4px 1.4px at 8% 45%, rgba(255,255,255,.58), transparent),
    radial-gradient(1.5px 1.5px at 92% 65%, rgba(255,255,255,.66), transparent),
    radial-gradient(1.3px 1.3px at 38% 89%, rgba(255,255,255,.6), transparent),
    radial-gradient(1.4px 1.4px at 78% 92%, rgba(255,255,255,.55), transparent),
    radial-gradient(1.2px 1.2px at 24% 61%, rgba(255,255,255,.5), transparent),
    radial-gradient(1.3px 1.3px at 68% 51%, rgba(255,255,255,.54), transparent),
    radial-gradient(1.2px 1.2px at 48% 74%, rgba(255,255,255,.5), transparent),
    radial-gradient(1.4px 1.4px at 16% 17%, rgba(255,255,255,.58), transparent),
    radial-gradient(1.3px 1.3px at 84% 25%, rgba(255,255,255,.52), transparent),
    radial-gradient(1.2px 1.2px at 70% 7%, rgba(255,255,255,.48), transparent),
    radial-gradient(1.3px 1.3px at 40% 44%, rgba(255,255,255,.5), transparent),
    radial-gradient(1.2px 1.2px at 94% 38%, rgba(255,255,255,.46), transparent);
}
@keyframes apMegaDrift{
  0%,100%{translate:0 0}
  50%{translate:-7px -11px}
}
@keyframes apMegaTwinkle{
  0%,100%{opacity:.62}
  50%{opacity:1}
}
/* The sky only moves while the menu is open. The closed panel keeps its
   layout (it is hidden with opacity and visibility, not display), so ten
   animated layers behind it would have kept the compositor busy on every
   frame of every page, for something nobody can see. */
.ap-nav-drop:hover .ap-mega-stars::before,
.ap-nav-drop:focus-within .ap-mega-stars::before{
  animation:apMegaDrift calc(26s + var(--i,0) * 3s) ease-in-out calc(var(--i,0) * -2.2s) infinite,
            apMegaTwinkle calc(4.6s + var(--i,0) * .5s) ease-in-out calc(var(--i,0) * -.8s) infinite;
}
.ap-nav-drop:hover .ap-mega-stars::after,
.ap-nav-drop:focus-within .ap-mega-stars::after{
  animation:apMegaDrift calc(38s + var(--i,0) * 4s) ease-in-out calc(var(--i,0) * 3.1s) infinite reverse,
            apMegaTwinkle calc(6.8s + var(--i,0) * .7s) ease-in-out calc(var(--i,0) * -1.6s) infinite;
}

/* the artwork takes the card on hover, and the sky gets out of its way */
.ap-mega-card:hover .ap-mega-stars,
.ap-mega-card:focus-visible .ap-mega-stars{opacity:0}

.ap-mega-img{
  position:absolute; inset:0; display:block; overflow:hidden;
  opacity:0;
  transition:opacity .4s ease;
}
.ap-mega-card:hover .ap-mega-img,
.ap-mega-card:focus-visible .ap-mega-img{opacity:1}
.ap-mega-img img{
  display:block; width:100%; height:100%; object-fit:cover;
  transform:scale(1.06);
  transition:transform .5s ease;
}
.ap-mega-card:hover .ap-mega-img img{transform:scale(1)}
/* scrim under the text once the artwork is showing */
.ap-mega-card::after{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(180deg, rgba(6,7,12,0) 36%, rgba(6,7,12,.72) 72%, rgba(6,7,12,.92) 100%);
  opacity:0; transition:opacity .4s ease;
}
.ap-mega-card:hover::after,
.ap-mega-card:focus-visible::after{opacity:1}

/* Everything sits on the card's floor in both states; hover just reveals
   the artwork and lifts the block a touch. The arrow rides the same lift so
   it stays on the description's baseline. */
.ap-mega-text{
  position:absolute; left:0; right:0; bottom:18px; z-index:1;
  padding:0 16px;
  transition:transform .35s cubic-bezier(.2,.7,.2,1);
}
.ap-mega-card:hover .ap-mega-text{transform:translateY(-12px)}
.ap-mega-title{
  display:block; margin:0;
  font-family:'Anton',sans-serif; font-weight:400;
  font-size:min(35px, 13.5cqw); line-height:1.04; letter-spacing:.015em;
  text-transform:uppercase; color:var(--paper);
}
.ap-mega-desc{
  display:block; margin:6px 0 0;
  padding-right:46px;
  font-size:16px; font-weight:300; letter-spacing:normal; line-height:1.5;
  color:rgba(238,240,246,.62);
}
.ap-mega-arrow{
  position:absolute; right:16px; bottom:14px; z-index:2;
  font-style:normal; font-size:24px; line-height:1; color:var(--orange);
  transition:transform .35s cubic-bezier(.2,.7,.2,1);
}
.ap-mega-card:hover .ap-mega-arrow{transform:translate(2px,-14px)}

@media (prefers-reduced-motion: reduce){
  .ap-mega-img,.ap-mega-text,.ap-mega-card::after,.ap-mega-arrow{transition:none}
}
/* the old narrow dropdown look no longer applies where the mega exists, and
   below the desktop nav breakpoint the mobile menu takes over anyway */
@media (max-width:1024px){
  .ap-mega{display:none}
}
.ap-nav-dropdown a{
  padding:11px 13px; border-radius:8px; font-size:12px; font-weight:700;
  letter-spacing:.05em; text-transform:none; color:rgba(238,240,246,.78); white-space:nowrap;
  transition:background .2s ease, color .2s ease;
}
.ap-nav-dropdown a:hover{background:rgba(255,90,20,.12); color:var(--paper)}
.ap-cta{
  --mx:50%;
  --my:50%;

  position:relative;
  isolation:isolate;
  justify-self:end;

  border:1px solid rgba(255,90,20,.72);
  background:
    radial-gradient(
      circle 90px at var(--mx) var(--my),
      rgba(255,125,55,.16),
      rgba(255,90,20,.055) 42%,
      transparent 75%
    ),
    rgba(10,8,24,.55);

  font-size:12.5px;
  font-weight:700;
  letter-spacing:.12em;
  padding:13px 24px;
  border-radius:8px;
  cursor:pointer;
  display:flex;
  align-items:center;
  gap:10px;

  transition:
    border-color .35s ease,
    box-shadow .35s ease,
    transform .35s cubic-bezier(.2,.8,.2,1),
    background .2s ease;
}

/* halo exterior */
.ap-cta::before{
  content:'';
  position:absolute;
  z-index:-1;
  inset:-18px;
  border-radius:18px;
  pointer-events:none;

  background:
    radial-gradient(
      circle 75px at var(--mx) var(--my),
      rgba(255,90,20,.34),
      rgba(255,90,20,.10) 40%,
      transparent 72%
    );

  opacity:0;
  filter:blur(12px);
  transition:opacity .35s ease;
}

/* barrido luminoso por encima */
.ap-cta::after{
  content:'';
  position:absolute;
  inset:-1px;
  border-radius:8px;
  pointer-events:none;

  background:
    linear-gradient(
      110deg,
      transparent 20%,
      rgba(255,255,255,0) 37%,
      rgba(255,220,195,.72) 48%,
      rgba(255,108,35,.9) 52%,
      rgba(255,255,255,0) 63%,
      transparent 80%
    );

  background-size:260% 100%;
  background-position:160% 0;

  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask-composite:exclude;

  padding:1px;
  opacity:0;

  transition:
    opacity .25s ease,
    background-position .8s cubic-bezier(.16,1,.3,1);
}

.ap-cta:hover{
  border-color:rgba(255,132,66,.95);

  box-shadow:
    0 0 18px rgba(255,90,20,.22),
    0 0 48px rgba(255,90,20,.10),
    inset 0 0 18px rgba(255,90,20,.055);

  transform:translateY(-1px);
}

.ap-cta:hover::before{
  opacity:1;
}

.ap-cta:hover::after{
  opacity:1;
  background-position:-60% 0;
}

.ap-cta span{
  color:var(--orange);
  transition:
    transform .35s cubic-bezier(.2,.8,.2,1),
    filter .35s ease;
}

.ap-cta:hover span{
  transform:translate(2px,-2px);
  filter:drop-shadow(0 0 6px rgba(255,90,20,.8));
}
.ap-cta span{color:var(--orange)}

/* language switcher, sits at the end of the nav (desktop) / mnav (mobile) */
.ap-lang-switch{display:inline-flex; align-items:center; gap:8px; margin-left:6px; padding-left:14px; border-left:1px solid var(--line)}
.ap-lang-switch a{color:rgba(238,240,246,.45); transition:color .2s}
.ap-lang-switch a:hover{color:var(--paper)}
.ap-lang-switch a.is-current{color:var(--orange)}
.ap-lang-switch--mobile{display:none}

/* hamburger + dropdown, hidden entirely above the mobile breakpoint,
   where the inline .ap-nav already handles it */
.ap-burger{display:none}
.ap-mnav{display:none}

/* ---------- typography ---------- */
.ap-eyebrow{
  font-family:'Space Mono',monospace; font-size:12px; letter-spacing:.44em;
  color:rgba(238,240,246,.72); text-indent:.44em; margin-bottom:26px;
}
.ap-display{
  font-family:'Anton',sans-serif; font-weight:400;
  /* the floor is what a phone gets: the 4vw term only overtakes it around
     850px, so this leaves every desktop size untouched */
  font-size:clamp(34px,4vw,80px); line-height:1.04; letter-spacing:.005em;
  margin:0; color:#f2f3f8;
}
.ap-grad{
  background:linear-gradient(100deg,#ff8a3d 0%,#ff5a14 45%,#e63c00 100%);
  -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
  color:var(--orange); filter:drop-shadow(0 0 40px rgba(255,90,20,.32));
}
.ap-lead{
  max-width:660px; margin:28px auto 0; font-size:17.5px; line-height:1.62;
  color:rgba(238,240,246,.86); text-wrap:pretty;
}
.ap-lead b{color:var(--orange); font-weight:500}

/* ---------- shared buttons ---------- */
/* Big solid-orange CTA (art direction 2026-09): the one deliberately loud
   button in the row, everything else on the site stays outlined. */
.ap-more{
  display:inline-flex; align-items:center; justify-content:center; gap:14px;
  padding:20px 36px; border-radius:10px;
  background:var(--orange); color:#fff;
  font-size:14px; font-weight:700; letter-spacing:.18em;
  transition:box-shadow .25s, transform .25s, filter .25s;
}
.ap-more i{
  font-style:normal; font-size:19px; line-height:1; color:#fff;
  transition:transform .25s;
}
.ap-more:hover{
  /* the global a:hover{color:orange} outranks .ap-more's white and painted
     the label orange-on-orange (it looked like the text vanished) */
  color:#fff;
  background:#ff7d2e;   /* hotter step up from --orange, so the state reads */
  box-shadow:0 0 30px rgba(255,90,20,.45);
  transform:translateY(-2px);
}
.ap-more:hover i{transform:translate(3px,-3px)}

/* One button voice across the site (art direction 2026-09): outlined pill on
   the dark ground, wide-tracked uppercase, orange arrow. The old solid-orange
   variant folds into this same look; the class names stay so nothing else has
   to change. */
.ap-btn{
  font-size:12.5px; font-weight:700; letter-spacing:.2em; padding:16px 30px;
  border-radius:10px; cursor:pointer; display:inline-flex; align-items:center; gap:12px;
  border:1px solid rgba(255,90,20,.75);
  background:rgba(10,8,24,.45);
  color:var(--paper);
  transition:box-shadow .25s, transform .25s, border-color .25s, background .25s;
}
.ap-btn span{color:var(--orange)}
.ap-btn:hover{
  border-color:rgba(255,132,66,.95);
  background:rgba(255,90,20,.08);
  box-shadow:0 0 22px rgba(255,90,20,.25);
  transform:translateY(-1px);
}
.ap-btn--solid{background:rgba(255,90,20,.1)}
.ap-btn--ghost{border-color:rgba(255,90,20,.45)}

/* ---------- pointer-tracked glow ----------
   The navbar CTA lights up wherever the pointer is over it. This gives the
   same behaviour to every other button. --mx/--my are the pointer position
   as a percentage of the button's own box, written by global.js; the 50%
   defaults keep it centred and harmless if JS never runs.

   Two layers, both driven by the same coordinates:
     ::before  the light ON the button. Same gradient values as .ap-cta,
               copied deliberately: an earlier pass used a lighter, wider one
               and the new buttons read visibly brighter than the navbar.
     ::after   a soft halo spilling OUTSIDE the box, blurred and pushed
               behind the button with a negative z-index.

   Deliberately NOT merged with .ap-cta, which keeps its own version: that
   button also runs a border sweep on hover, and an element only has two
   pseudo-elements to spend. The shared JS drives both.

   Pseudo-elements rather than an extra background layer, because the two
   hero buttons come from the Claude Design export with their background set
   in an inline style attribute, which no stylesheet rule can override. */
.ap-btn,
.ap-more,
.ap-foot-cta,
.ap-hero-btn{
  --mx:50%;
  --my:50%;
  position:relative;
  /* new stacking context, so the negative z-index on ::after stays behind the
     button and does not fall through to whatever is behind the section */
  isolation:isolate;
}

.ap-btn::before,
.ap-more::before,
.ap-foot-cta::before,
.ap-hero-btn::before{
  content:'';
  position:absolute;
  inset:0;
  border-radius:inherit;
  pointer-events:none;
  /* Both layers go behind the label (a positioned pseudo-element with
     z-index auto would paint OVER the text and tint it), and the light goes
     behind the halo, which is the order .ap-cta ends up with: there the
     light is part of the button's own background and the halo is a negative
     z-index pseudo-element painting on top of it. */
  z-index:-2;
  background:radial-gradient(
    circle 90px at var(--mx) var(--my),
    rgba(255,125,55,.16),
    rgba(255,90,20,.055) 42%,
    transparent 75%
  );
  opacity:0;
  transition:opacity .35s ease;
}

.ap-btn::after,
.ap-more::after,
.ap-foot-cta::after,
.ap-hero-btn::after{
  content:'';
  position:absolute;
  inset:-18px;
  z-index:-1;   /* over the inner light, under the label (see ::before) */
  border-radius:18px;
  pointer-events:none;
  background:radial-gradient(
    circle 75px at var(--mx) var(--my),
    rgba(255,90,20,.34),
    rgba(255,90,20,.10) 40%,
    transparent 72%
  );
  filter:blur(12px);
  opacity:0;
  transition:opacity .35s ease;
}

.ap-btn:hover::before,  .ap-btn:hover::after,
.ap-more:hover::before, .ap-more:hover::after,
.ap-foot-cta:hover::before, .ap-foot-cta:hover::after,
.ap-hero-btn:hover::before, .ap-hero-btn:hover::after{
  opacity:1;
}


/* Its arrow already has its own hover treatment; keep the moving light off
   the circle so the two don't fight. */
.ap-more i{position:relative; z-index:1}

@media (prefers-reduced-motion: reduce){
  .ap-btn::before,  .ap-btn::after,
  .ap-more::before, .ap-more::after,
  .ap-foot-cta::before, .ap-foot-cta::after,
  .ap-hero-btn::before, .ap-hero-btn::after{
    transition:none;
  }
}

/* ---------- CTA band ----------
   Big centred "ready when you are" section pattern, used at the bottom of
   the home page and reused as-is on any content page (services, etc). */
.ap-cta-band{position:relative; text-align:center; padding:150px 0 170px; overflow:hidden}
.ap-cta-band::before{
  content:''; position:absolute; left:50%; bottom:-42vh; transform:translateX(-50%);
  width:130vw; height:80vh; pointer-events:none; border-radius:50%;
  background:radial-gradient(ellipse 50% 50% at 50% 50%, rgba(255,90,20,.16) 0%, rgba(255,90,20,0) 68%);
}
.ap-buttons{display:flex; align-items:center; justify-content:center; gap:18px; margin-top:36px; flex-wrap:wrap}

/* ---------- footer ---------- */
/* dark enough to smother the page's ambient corner glow where it would
   otherwise bleed through as a stray warm patch, translucent enough to
   still let the starfield show through */
/* The ground fades in instead of starting on a line. The flat fill plus a
   border-top was the hard edge under the CTA's moon: a 1px rule and an
   86% black step against whatever the section above ends on. */
.ap-foot{
  position:relative; z-index:1; padding:90px 0 0; overflow:hidden;
  background:linear-gradient(180deg,rgba(3,3,5,0) 0,rgba(3,3,5,.86) 120px);
}
.ap-foot-top{
  position:relative; z-index:2;
  display:grid; grid-template-columns:1.5fr 1fr 1fr 1fr; gap:40px;
  padding-bottom:58px;
}
.ap-foot-brand{display:flex; flex-direction:column; gap:20px; max-width:320px}
.ap-foot-logo img{height:28px; width:auto}
.ap-foot-tagline{font-size:14.5px; line-height:1.65; color:var(--muted); text-wrap:pretty; margin:0}
.ap-foot-col{display:flex; flex-direction:column; gap:14px}
.ap-foot-head{
  font-family:'Space Mono',monospace; font-size:11px; letter-spacing:.22em;
  color:rgba(238,240,246,.45); margin-bottom:4px;
}
.ap-foot-col a{font-size:14.5px; color:rgba(238,240,246,.78); transition:color .25s}
.ap-foot-col a:hover{color:var(--orange)}
.ap-foot-cta{
  display:inline-flex; align-items:center; gap:10px; margin-top:6px;
  border:1px solid var(--orange); background:rgba(255,90,20,.08);
  font-size:12.5px; font-weight:700; letter-spacing:.1em; padding:12px 20px;
  border-radius:8px; cursor:pointer; width:fit-content; transition:box-shadow .25s;
}
.ap-foot-cta:hover{box-shadow:0 0 22px rgba(255,90,20,.35)}
.ap-foot-cta span{color:var(--orange)}
.ap-foot-bottom{
  position:relative; z-index:2;
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:26px 0; border-top:1px solid var(--line);
  font-family:'Space Mono',monospace; font-size:11.5px; letter-spacing:.04em; color:rgba(238,240,246,.45);
}
.ap-foot-legal{display:flex; gap:26px}
.ap-foot-legal a{color:rgba(238,240,246,.45); transition:color .25s}
.ap-foot-legal a:hover{color:var(--orange)}

/* Cookie banner (global.js, "consent banner"): one small card in the
   corner, both answers the same size so neither is the easy default. */
.ap-consent{
  position:fixed; left:16px; bottom:16px; z-index:190;
  display:flex; align-items:center; gap:14px;
  max-width:440px; padding:10px 10px 10px 14px;
  background:rgba(3,3,5,.92); border:1px solid var(--line); border-radius:10px;
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  font-family:'Space Mono',monospace; font-size:11px; line-height:1.45; color:rgba(238,240,246,.72);
}
.ap-consent p{margin:0}
.ap-consent a{color:var(--paper); text-decoration:underline; text-underline-offset:2px}
.ap-consent-btns{display:flex; gap:6px; flex-shrink:0}
.ap-consent button{
  min-width:74px; padding:7px 10px; border-radius:6px; cursor:pointer;
  font:inherit; font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--paper); background:transparent; border:1px solid rgba(238,240,246,.35);
}
.ap-consent button:hover, .ap-consent button:focus-visible{border-color:var(--orange); color:var(--orange); outline:none}
@media (max-width:520px){
  .ap-consent{left:10px; right:10px; bottom:10px; max-width:none; flex-direction:column; align-items:stretch; gap:8px}
  .ap-consent-btns button{flex:1}
}

@media (max-width:1024px){
  .ap-foot{padding:64px 0 0}
  .ap-foot-top{grid-template-columns:1fr 1fr; gap:36px 30px; padding-bottom:42px}
  .ap-foot-brand{grid-column:1 / -1; max-width:460px}
}

/* ---------- reveal on scroll ----------
   Only hide things when scripting is actually available, so a JS failure
   (or an old browser) can never leave the page blank. */
@media (scripting: enabled){
  .ap-rv{opacity:0; transform:translateY(26px)}
}
/* An animation, not a transition.
   The reveal used to be a transition declared on .ap-rv, which meant any
   component that declared a `transition` of its own replaced it: one class,
   same specificity, later in the file. .ap-work-card transitions its hover
   (border, translate, background) and so lost the fade entirely, and the
   listing's cards snapped in with no animation at all. `animation` is a
   separate property, so a component's transition cannot reach it. */
.ap-rv.is-in{animation:apReveal .7s cubic-bezier(.22,.61,.36,1) both}
@keyframes apReveal{
  from{opacity:0; transform:translateY(26px)}
  to{opacity:1; transform:none}
}

/* ---------- the page title is never hidden ----------
   The h1 is the largest thing on every hero, so it is what Chrome reports as
   the Largest Contentful Paint, and Chrome does not count an element while
   its opacity is 0. As a scroll reveal it stayed invisible until global.js
   had loaded, the observer had fired and the fade had run: measured on a
   throttled phone, the title landed 1.2 to 1.7s after the first paint.

   It still rises, but by transform alone and from the first style pass, so
   it is visible, and counted, the moment the page paints, and it no longer
   waits for any script. The selector outranks .ap-rv.is-in, so the reveal
   does not replay when the observer catches up. */
h1.ap-display.ap-rv{opacity:1; transform:none; animation:apTitleRise .8s cubic-bezier(.19,.82,.22,1) both}
@keyframes apTitleRise{
  from{transform:translateY(22px)}
  to{transform:none}
}
@media (prefers-reduced-motion:reduce){
  h1.ap-display.ap-rv{animation:none}
}

/* ---------- responsive ---------- */
@media (max-width:1024px){
  .ap-shell{width:min(var(--shell),100% - 48px)}
  .ap-cta-band{padding:90px 0}
  .ap-cta-band .ap-planet{display:none}
  /* ...except the easter-egg planet: it was disappearing entirely off the
     mobile viewport before (not just hidden by the rule above, it was
     genuinely positioned outside the visible area), so it gets its own
     smaller, corner-anchored position instead of inheriting the desktop
     top:10%/right:2%/110px placement. !important because the element also
     carries an inline style="..." (top/right/width) from index.php, which
     otherwise wins over any stylesheet rule no matter how specific. */
  .ap-cta-band .ap-planet.ap-planet--egg{
    display:block !important; top:3% !important; right:4% !important; width:46px !important; opacity:.85 !important;
  }
  /* the sticky header is the ONLY header on mobile, shown from first paint
     instead of fading in after the hero scrolls by, since the hero's own
     inline nav is hidden (too many links for this width to hold) */
  .ap-header{
    padding:16px 20px; opacity:1; visibility:visible; transform:none; pointer-events:auto;
    /* the desktop bar centres the CTA between logo and nav with 1fr auto 1fr;
       with the nav gone there is nothing on its right to balance against, so
       it sat alone in the middle of the bar. Both controls belong together
       on the right. */
    grid-template-columns:1fr auto auto; column-gap:14px;
  }
  /* The veil's 64px bleed is sized for the desktop bar. Here the bar is
     70px tall and the page's first line lands around 108px, so that bleed
     reaches over it and the backdrop blur softens the eyebrow and the case
     breadcrumbs. Half of it clears them and still dissolves. */
  .ap-header::before{inset:-16px 0 -16px 0}
  .ap-header::after{inset:-24px -60px -30px -60px}
  .ap-nav{display:none}
  .ap-cta{grid-column:2; justify-self:end; padding:11px 18px; font-size:11.5px}
  .ap-burger{
    display:flex; flex-direction:column; align-items:flex-end; justify-content:center; gap:5px;
    grid-column:3; justify-self:end; width:30px; height:30px; padding:0; margin:0;
    background:none; border:0; cursor:pointer;
  }
  .ap-burger span{
    display:block; width:22px; height:2px; border-radius:2px; background:var(--paper);
    transition:transform .3s cubic-bezier(.22,.61,.36,1), opacity .25s ease, width .25s ease;
  }
  .ap-burger.is-open span:nth-child(1){transform:translateY(7px) rotate(45deg)}
  .ap-burger.is-open span:nth-child(2){opacity:0; width:0}
  .ap-burger.is-open span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
  .ap-mnav{
    display:flex; flex-direction:column;
    position:absolute; top:100%; left:0; right:0; z-index:5;
    max-height:0; overflow:hidden; opacity:0; pointer-events:none;
    background:rgba(6,7,15,.97); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
    border-top:1px solid var(--line);
    transition:max-height .45s cubic-bezier(.22,.61,.36,1), opacity .3s ease;
  }
  .ap-mnav.is-open{max-height:70vh; opacity:1; pointer-events:auto; overflow-y:auto}
  .ap-mnav a{
    padding:18px 24px; font-size:15px; font-weight:700; letter-spacing:.09em;
    border-bottom:1px solid var(--line); color:var(--paper);
  }
  .ap-mnav a.is-current{color:var(--orange)}
  /* Services and Industries open a sub-list instead of navigating, and
     nothing said so. global.js already puts aria-expanded on exactly those
     two, so the chevron needs no markup and no script of its own. */
  .ap-mnav a[aria-expanded]{
    display:flex; align-items:center; justify-content:space-between; gap:12px;
  }
  .ap-mnav a[aria-expanded]::after{
    content:''; flex:none; width:9px; height:9px; margin-right:2px;
    border-right:2px solid currentColor; border-bottom:2px solid currentColor;
    transform:rotate(45deg) translate(-2px,-2px);
    transition:transform .25s cubic-bezier(.22,.61,.36,1);
    opacity:.75;
  }
  .ap-mnav a[aria-expanded="true"]::after{
    transform:rotate(-135deg) translate(-3px,-3px); opacity:1;
  }
  .ap-mnav-sub{
  display:none;
  flex-direction:column;
  position:relative;
  padding:4px 24px 12px 48px;
  border-bottom:1px solid var(--line);
}
.ap-mnav-sub::before{
  content:'';
  position:absolute;
  left:24px; top:12px; bottom:16px;
  width:2px;
  background:rgba(238,240,246,.4);
  pointer-events:none;
}

.ap-mnav-sub.is-open{
  display:flex;
}

.ap-mnav-sub a{
  display:block;
  width:100%;
  padding:13px 0;
  min-height:50px;
  font-size:17px;
  line-height:1.4;
  font-weight:600;
  letter-spacing:.01em;
  color:rgba(238,240,246,.88);
  border-bottom:0;
}
  .ap-lang-switch--mobile{
  display:grid;
  grid-template-columns:1fr 1fr;
  width:100%;
  gap:0;
  padding:0;
  margin:0;
  border-left:0;
}

.ap-lang-switch--mobile a{
  width:100%;
  padding:20px 10px;
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:13px;
  font-weight:700;
  letter-spacing:.09em;
  border-bottom:1px solid var(--line);
}

.ap-lang-switch--mobile a + a{
  border-left:1px solid var(--line);
}

  .ap-lang-switch--mobile a{font-size:13px; font-weight:700; letter-spacing:.09em}
}
@media (max-width:560px){
  .ap-shell{width:calc(100% - 32px)}
  .ap-cta{padding:11px 16px; font-size:11px}
  .ap-foot-top{grid-template-columns:1fr; gap:34px; padding-bottom:34px}
  .ap-foot-bottom{flex-direction:column; align-items:flex-start; gap:14px}
}

@media (prefers-reduced-motion:reduce){
  .ap-rv,.ap-rv.is-in{opacity:1; transform:none; animation:none}
}

/* ==========================================================================
   contact modal, triggered by any [data-ap-contact-trigger] (header CTA,
   footer CTA), from any page. Three layers, entrance staged bottom-to-top
   (background flashes in first, then the ship rises into place, then the
   form/card panel), closing runs the same layers in reverse with shorter,
   undelayed transitions, see the .is-closing overrides below each layer.
   State toggling and timing live in global.js; everything about *how* each
   layer moves lives here. ========================================================================== */
.ap-cm{
  position:fixed; inset:0; z-index:200;
  display:flex; align-items:stretch;
}
.ap-cm[hidden]{display:none}

/* position:fixed (not absolute) on bg/close, both need to stay put even
   when .ap-cm itself becomes an internally-scrolling container on mobile
   (see the media query below), and pin to the same box either way since
   .ap-cm already exactly covers the viewport */
.ap-cm-bg{
  position:fixed; inset:0; z-index:0;
  background-size:cover; background-position:center;
  opacity:0; transition:opacity .3s ease;
}
/* darkens the panel side regardless of how bright the source photo is, the nebula asset is dark already, this is just insurance for contrast */
.ap-cm-bg::after{
  content:''; position:absolute; inset:0;
  background:linear-gradient(90deg, rgba(3,3,5,.74) 0%, rgba(3,3,5,.46) 46%, rgba(3,3,5,.08) 74%);
}
.ap-cm.is-open .ap-cm-bg{opacity:1; transition:opacity .32s ease}
.ap-cm.is-closing .ap-cm-bg{transition:opacity .3s ease .38s}

.ap-cm-close{
  position:fixed; top:26px; right:32px; z-index:4;
  width:44px; height:44px; border-radius:50%;
  border:1px solid rgba(238,240,246,.28); background:rgba(3,3,5,.5); backdrop-filter:blur(8px);
  color:var(--paper); display:grid; place-items:center; cursor:pointer;
  transition:border-color .25s, color .25s, box-shadow .25s, transform .25s;
}
.ap-cm-close:hover{border-color:var(--orange); color:var(--orange); box-shadow:0 0 20px rgba(255,90,20,.3); transform:rotate(90deg)}

/* ---- ship: pinned to the right edge (order:1 puts it after the panel in
   visual order without touching DOM order, it's aria-hidden, so tab order
   doesn't care), overflow:hidden so only a window onto the porthole is ever
   visible. The BOX itself never moves, .ap-cm-ship-img is what animates on
   open/close (see below). Doing it this way (a static clip window with the
   image travelling through it) instead of animating the box lets the nose
   cone genuinely rise into frame and back out the top as it settles, rather
   than staying invisible the entire time behind a crop window that would
   otherwise be rigidly attached to, and travelling with, the moving box. */
.ap-cm-ship{
  position:relative; z-index:1; order:1; flex:0 0 min(50vw,700px);
  overflow:hidden; pointer-events:none;
}

.ap-cm-ship-img{
  position:absolute; left:50%; bottom:-10%; z-index:1;
  width:auto; height:140%; max-width:none;
  transform:translate(-50%,130vh);
  transition:transform .55s cubic-bezier(.4,0,.2,1);
  /* tune bottom/height above to line up the porthole with the crop window, this is deliberately a rough default, easiest to eyeball in-browser */
}
.ap-cm.is-open .ap-cm-ship-img{transform:translate(-50%,0); transition:transform .75s cubic-bezier(.16,1,.3,1) .12s}
.ap-cm.is-closing .ap-cm-ship-img{transition:transform .5s cubic-bezier(.4,0,.2,1) .15s}

@keyframes apCmFloat{0%,100%{transform:translate(-50%,0)}50%{transform:translate(-50%,-11px)}}
.ap-cm.is-open .ap-cm-ship-img{animation:apCmFloat 6.5s ease-in-out .95s infinite}

/* ---- panel: form + info card, slides in after the ship settles */
.ap-cm-panel{
  position:relative; z-index:2; flex:1;
  display:flex; flex-direction:column; justify-content:safe center; /* safe: a form taller than the window must not lose its top */ align-items:flex-end;
  padding:120px min(9vw,88px) 48px;
  max-height:100vh; overflow-y:auto;
  scrollbar-width:none; -ms-overflow-style:none; /* stays scrollable on a short window, just no visible rail */
  opacity:0; transform:translateX(-26px);
  transition:opacity .3s ease, transform .3s ease;
  /* align-items:flex-end hugs the content against the ship's edge (now on
     the right) instead of the outer viewport edge, that's what actually
     read as "centered" before the ship moved sides: the panel's own edge
     touching the ship, not literal viewport-center */
}
.ap-cm.is-open .ap-cm-panel{opacity:1; transform:none; transition:opacity .5s ease .52s, transform .5s cubic-bezier(.2,.8,.2,1) .52s}
.ap-cm.is-closing .ap-cm-panel{transition:opacity .22s ease, transform .22s ease}

/* single block the panel aligns as one unit, head/form/card all sit at
   width:100% of THIS, so they share one left edge instead of each being
   independently right-aligned to its own shrink-to-fit width (which is what
   produced the ragged edge: a heading, a form and a card all measure
   different "natural" widths) */
.ap-cm-inner{width:100%; max-width:560px; display:flex; flex-direction:column; gap:28px}

.ap-cm-head{max-width:460px}
.ap-cm-head .ap-display{font-size:clamp(26px,3vw,40px); margin-top:14px}
.ap-cm-sub{margin:14px 0 0; font-size:15px; line-height:1.6; color:var(--muted); text-wrap:pretty}

.ap-cm-hp{position:absolute; left:-9999px; width:1px; height:1px; opacity:0}
.ap-cm-status{margin:0; min-height:1.4em; font-size:12.5px; font-weight:600; color:var(--muted)}
.ap-cm-status[data-state="ok"]{color:var(--orange-soft)}
.ap-cm-status[data-state="error"]{color:#ff8080}

.ap-cm-card{
  max-width:440px; padding:20px 22px; border-radius:16px;
  background:rgba(15,18,33,.82); border:1px solid rgba(238,240,246,.12);
  box-shadow:0 20px 50px -24px rgba(0,0,0,.6);
}
.ap-cm-card-head{font-family:'Space Mono',monospace; font-size:11px; letter-spacing:.2em; color:rgba(238,240,246,.5); margin-bottom:10px}
.ap-cm-card-email{display:inline-block; font-size:15px; font-weight:700; color:var(--orange-soft)}
.ap-cm-card-email:hover{color:var(--orange)}
.ap-cm-card-note{margin:8px 0 0; font-size:13px; line-height:1.55; color:var(--muted)}

.ap-cm-panel::-webkit-scrollbar{display:none}

/* ---- the form: shared with /contact/ (includes/contact-form.php) ---- */
.ap-cf{
  display:flex; flex-direction:column; gap:clamp(16px,2.2vh,22px);
  padding:clamp(24px,3.4vw,44px);
  border:1px solid var(--line); border-radius:22px;
  background:linear-gradient(180deg,rgba(17,19,26,.9),rgba(8,9,13,.92));
}
.ap-cf-row{display:grid; grid-template-columns:1fr 1fr; gap:clamp(14px,1.6vw,22px)}
.ap-cf-field{display:flex; flex-direction:column; gap:9px; min-width:0; border:0; padding:0; margin:0}
.ap-cf-field > label,.ap-cf-field > legend{
  padding:0; font-size:14.5px; font-weight:400; color:rgba(238,240,246,.9);
}
/* the asterisk is the only orange in a label */
.ap-cf-field b{color:var(--orange); font-weight:400}
.ap-cf-hint{margin:8px 0 16px; font-size:13px; font-weight:300; color:rgba(238,240,246,.45)}

.ap-cf-field input[type="text"],
.ap-cf-field input[type="email"],
.ap-cf-field textarea,
.ap-cf-select select{
  width:100%; font:inherit; font-size:15px; color:var(--paper);
  padding:14px 16px; border-radius:12px;
  border:1px solid var(--line); background:rgba(6,7,10,.75);
  transition:border-color .2s ease, background-color .2s ease;
}
.ap-cf-field textarea{resize:vertical; min-height:150px; line-height:1.55}
.ap-cf-field input::placeholder,.ap-cf-field textarea::placeholder{color:rgba(238,240,246,.33)}
.ap-cf-field input:focus,.ap-cf-field textarea:focus,.ap-cf-select select:focus{
  outline:none; border-color:rgba(255,90,20,.6); background:rgba(6,7,10,.95);
}

/* a select keeps the same shell as an input, with our own chevron: the
   native one is drawn by the OS and ignores the palette */
.ap-cf-select{position:relative; display:block}
/* a long option must not run under the chevron */
.ap-cf-select select{appearance:none; padding-right:44px; cursor:pointer; text-overflow:ellipsis}
.ap-cf-select option{background:#0b0d12; color:var(--paper)}
.ap-cf-select i{
  position:absolute; right:16px; top:50%; translate:0 -50%;
  display:inline-flex; font-size:11px; color:rgba(238,240,246,.5); pointer-events:none;
}

/* ---------- the speciality chips ---------- */
.ap-cf-chips{gap:0}
.ap-cf-chip-grid{display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px}
.ap-cf-chip{position:relative; display:block}
/* the box itself is only there for the keyboard and the form data */
.ap-cf-chip input{position:absolute; inset:0; width:100%; height:100%; margin:0; opacity:0; cursor:pointer}
.ap-cf-chip span{
  display:flex; align-items:center; justify-content:center; text-align:center;
  min-height:48px; padding:10px 12px; border-radius:12px;
  border:1px solid var(--line); background:rgba(6,7,10,.6);
  font-size:14.5px; color:rgba(238,240,246,.82);
  transition:border-color .2s ease, background-color .2s ease, color .2s ease;
}
.ap-cf-chip:hover span{border-color:rgba(255,90,20,.45); color:var(--paper)}
.ap-cf-chip input:checked + span{
  background:var(--orange); border-color:var(--orange); color:#0b0d12; font-weight:500;
}
.ap-cf-chip input:focus-visible + span{outline:2px solid var(--orange); outline-offset:3px}

/* ---------- consent ---------- */
.ap-cf-consent{
  display:flex; align-items:center; gap:14px; margin-top:6px; cursor:pointer;
  font-size:14.5px; font-weight:300; color:rgba(238,240,246,.82);
}
.ap-cf-consent input{
  appearance:none; flex:0 0 auto; width:26px; height:26px; margin:0; cursor:pointer;
  border:1px solid rgba(238,240,246,.55); border-radius:7px; background:transparent;
  transition:border-color .2s ease, background-color .2s ease;
}
.ap-cf-consent input:checked{
  background:var(--orange) no-repeat center/13px 10px; border-color:var(--orange);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 10' fill='none' stroke='%230b0d12' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 5.2 4.7 8.8 12 1.4'/%3E%3C/svg%3E");
}
.ap-cf-consent input:focus-visible{outline:2px solid var(--orange); outline-offset:3px}
.ap-cf-consent a{color:inherit; text-decoration:underline; text-underline-offset:3px}
.ap-cf-consent a:hover{color:var(--orange)}

/* ---------- submit ----------
   The site's .ap-btn--solid is a tinted ghost, which is right for a button
   sitting among others. This one is the only action in its panel, so it is
   filled: solid orange with dark type, arrow included. */
.ap-cf-submit{
  width:100%; justify-content:center; margin-top:6px;
  background:var(--orange); border-color:var(--orange); color:#0b0d12;
}
/* .ap-btn paints its label span orange, which is invisible here */
.ap-cf-submit span{color:#0b0d12}
.ap-cf-submit .ap-arw{fill:currentColor}
/* the pointer-tracked glow is an orange wash, invisible on orange and it
   only muddies the fill; the outside halo stays */
.ap-cf-submit::before{display:none}
.ap-cf-submit:hover{background:var(--orange-soft); border-color:var(--orange-soft); color:#0b0d12}
.ap-cf-submit:disabled{opacity:.55; cursor:progress}
.ap-cf-required{margin:0; font-size:13px; font-weight:300; color:rgba(238,240,246,.45)}
.ap-cf-status:empty{display:none}
@media (max-width:620px){
  .ap-cf-row{grid-template-columns:minmax(0,1fr)}
  .ap-cf-chip-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
/* In the modal the form sits straight on the sky: no panel of its own, and
   a column wide enough for the two-up rows and the chips. */
.ap-cm .ap-cf{padding:0; border:0; background:none}
.ap-cm .ap-cf-field textarea{min-height:110px}

/* body scroll lock while a modal is open (shared with the invaders easter
   egg modal below, and any future one) */
html.ap-modal-lock{overflow:hidden}

@media (max-width:1024px){
  /* mobile priority is the form, not the ship: the form is what's visible
     the moment the modal opens, full stop, the ship sits BELOW it (order:1,
     after the panel, "inverted" from the desktop arrangement where it comes
     first) and only comes into view if you scroll down past the form. That
     sidesteps every version of "the ship covers the form" for good, instead
     of trying to carve out separate space for both. .ap-cm itself becomes
     the scroll container (overflow-y:auto), bg/close are position:fixed
     precisely so they stay put through that scroll instead of scrolling
     away with the content.

     The ship still lies on its side, the tall portrait asset doesn't fit a
     short band any other way, but no longer needs its own entrance
     animation: it's already below the fold when the modal opens, so it's
     simply sitting there, static, waiting to be scrolled to. */
  .ap-cm{flex-direction:column; overflow-y:auto}
  .ap-cm-ship{
    order:1; flex:0 0 30vh;
  }

  .ap-cm-ship-img{
    position:absolute; top:30%; left:50%; bottom:auto;
    width:auto; height:140vw; max-width:none;
    transform:translate(-50%,-50%) rotate(90deg);
    /* no object-fit here on purpose, that was cropping into the source
       image to force a fit, which is exactly what cut the nose off. Same
       approach as desktop: width:auto/height only, so the whole image
       renders at its own aspect ratio (oversized on purpose) and the band's
       overflow:hidden does the actual cropping. rotate(90deg), the mirror
       of desktop's untransformed nose-up orientation, turns the nose to
       point right; tune top/left/height above to line the porthole up
       further, same "eyeball it" approach as desktop. */
  }
  @keyframes apCmFloatM{
    0%,100%{transform:translate(-50%,-50%) rotate(90deg)}
    50%{transform:translate(-50%,calc(-50% - 10px)) rotate(90deg)}
  }
  .ap-cm.is-open .ap-cm-ship-img{animation:apCmFloatM 6.5s ease-in-out .95s infinite}
  /* exits downward on close, same direction the desktop ship disappears in
     (there it sinks back below the fold, reversing its own rise-up entry), the only ship transform mobile needs, since it has no entrance
     animation to begin with (see the comment up top) */
  .ap-cm.is-closing .ap-cm-ship-img{
    transform:translate(-50%,calc(-50% + 100vh)) rotate(90deg);
    transition:transform .45s cubic-bezier(.4,0,.2,1);
  }

  .ap-cm-panel{
    order:0;
    padding:14px 24px 40px; justify-content:flex-start; align-items:stretch;
    max-height:none; overflow-y:visible;
  }
  .ap-cm-bg::after{background:linear-gradient(180deg, rgba(3,3,5,.2) 0%, rgba(3,3,5,.5) 30%, rgba(3,3,5,.86) 46%, rgba(3,3,5,.92) 100%)}
  .ap-cm-close{top:18px; right:18px; width:38px; height:38px}
}

@media (prefers-reduced-motion:reduce){
  .ap-cm-bg, .ap-cm-ship-img, .ap-cm-panel{transition:none !important}
  .ap-cm-ship-img{animation:none !important}
}

/* ==========================================================================
   invaders easter egg, a lightweight modal wrapping an <iframe> onto
   astropanda-invaders-v2.html (untouched, lazy-loaded only while open, see
   includes/invaders-modal.php). Deliberately simpler than the contact modal:
   no staged entrance, just a quick fade+scale in and out, because the whole
   point is that it's trivial to open and even easier to bail out of. ========================================================================== */
.ap-planet--egg{
  /* .ap-planet is pointer-events:none by default (the other planets are
     purely decorative), this one specifically needs to catch clicks/taps.
     touch-action:manipulation kills the double-tap-to-zoom gesture on that
     element so a real double-tap reaches JS as two ordinary clicks instead
     of the browser eating the second one to zoom. */
  pointer-events:auto; cursor:pointer; touch-action:manipulation;
}

.ap-inv-modal{
  position:fixed; inset:0; z-index:210;
  display:flex; align-items:center; justify-content:center;
}
.ap-inv-modal[hidden]{display:none}

.ap-inv-modal-bg{
  position:fixed; inset:0; z-index:0;
  background:rgba(3,3,5,.86); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  opacity:0; transition:opacity .22s ease;
}
.ap-inv-modal.is-open .ap-inv-modal-bg{opacity:1}

.ap-inv-modal-close{
  position:fixed; top:22px; right:26px; z-index:2;
  width:42px; height:42px; border-radius:50%;
  border:1px solid rgba(238,240,246,.28); background:rgba(3,3,5,.6); backdrop-filter:blur(8px);
  color:var(--paper); display:grid; place-items:center; cursor:pointer;
  transition:border-color .25s, color .25s, box-shadow .25s, transform .25s;
}
.ap-inv-modal-close:hover{border-color:var(--orange); color:var(--orange); box-shadow:0 0 20px rgba(255,90,20,.3); transform:rotate(90deg)}

.ap-inv-modal-panel{
  position:relative; z-index:1;
  width:min(92vw,480px); height:min(90vh,820px);
  border-radius:14px; overflow:hidden;
  border:1px solid rgba(255,90,20,.3);
  box-shadow:0 30px 80px rgba(0,0,0,.6), 0 0 60px rgba(255,90,20,.12);
  opacity:0; transform:scale(.94);
  transition:opacity .22s ease, transform .28s cubic-bezier(.2,.8,.2,1);
}
.ap-inv-modal.is-open .ap-inv-modal-panel{opacity:1; transform:scale(1)}

.ap-inv-modal-frame{display:block; width:100%; height:100%; border:0; background:#07070c}

@media (max-width:640px){
  .ap-inv-modal-panel{width:100vw; height:100vh; border-radius:0; border:0}
  .ap-inv-modal-close{top:16px; right:16px}
}

@media (prefers-reduced-motion:reduce){
  .ap-inv-modal-bg, .ap-inv-modal-panel{transition:none !important}
}


/* ---------- the arrow ----------
   One shape (see ap_arrow() in includes/lang/bootstrap.php), rotated to
   whichever direction a given place needs. It inherits colour and sizes off
   font-size, so nothing that hosts it has to know it is an SVG. Rotation
   uses the `rotate` property, not `transform`: several of these sit inside
   labels whose hover already animates `translate`. */
.ap-arw{
  width:1em; height:.83em; flex:none;
  display:inline-block; vertical-align:-.07em;
  fill:currentColor;
}
.ap-arw--e{rotate:0deg}
.ap-arw--se{rotate:45deg}
.ap-arw--s{rotate:90deg}
.ap-arw--sw{rotate:135deg}
.ap-arw--w{rotate:180deg}
.ap-arw--nw{rotate:225deg}
.ap-arw--n{rotate:270deg}
.ap-arw--ne{rotate:315deg}
/* ==========================================================================
   The closing CTA: the animated lunar scene

   Was a static photograph set as a background on ::after. It is a poster
   image with a canvas over it now (includes/cta-band.php, and the motion in
   assets/js/lunar-cta.js), and it is on every page, which is why it lives
   here rather than in a page stylesheet. The plain .ap-cta-band underneath
   still works on its own for anything that does not want the scene.
   ========================================================================== */
.ap-cta-band--lunar{
  /* The art's own ratio, so the horizon lands where it was composed to land
     and the copy above it keeps the room it was drawn with. */
  aspect-ratio:1672 / 941;
  min-height:min(620px, 92vh);
  /* ...but the ratio must never decide the width. Below about 1100px the
     min-height is the binding constraint and the ratio answers with a
     width wider than the screen, which the band then hands to the
     document. Capped, the extra goes into height instead and the plate
     crops, which object-fit:cover already expects. */
  max-width:100%;
  padding:0; overflow:hidden; isolation:isolate;
  display:flex; flex-direction:column; justify-content:flex-start;
  background:#030507;
}
.ap-cta-plate,
.ap-cta-canvas{
  position:absolute; inset:0; z-index:0;
  width:100%; height:100%;
  object-fit:cover;
  pointer-events:none;
}
/* The canvas fades in over the poster once both plates have decoded, so the
   swap is never a flash of a different frame. */
.ap-cta-canvas{opacity:0; transition:opacity .5s ease}
.ap-cta-band--lunar[data-ready="true"] .ap-cta-canvas{opacity:1}
/* The base band's warm pool would tint a photograph that already carries
   its own light. */
.ap-cta-band--lunar::before{display:none}
/* Sinks the top of the scene so the heading reads over it, and the bottom
   so the section dissolves into the footer the way the old one did. */
.ap-cta-band--lunar::after{
  content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(180deg,rgba(3,5,7,.55),transparent 26%,transparent 86%,#030507);
}
.ap-cta-band--lunar .ap-shell{
  position:relative; z-index:2;
  /* held in the upper third: below it the panda and the planet need the room */
  padding-top:clamp(64px,10vh,150px); padding-bottom:40px;
}
.ap-cta-band--lunar .ap-display{font-size:clamp(40px,5.6vw,88px)}
.ap-cta-band--lunar .ap-lead{margin-top:20px}
.ap-cta-band--lunar .ap-planet--egg{z-index:2}

@media (max-width:900px){
  /* Portrait: the copy stacks above a bottom-anchored crop that keeps the
     panda readable, at the cost of some of the planet on the far left. */
  .ap-cta-band--lunar{aspect-ratio:auto; min-height:auto}
  .ap-cta-plate,
  .ap-cta-canvas{
    top:auto; bottom:0; height:auto; width:175%; max-width:none; left:-52%;
    aspect-ratio:1672 / 941; object-fit:contain;
  }
  .ap-cta-band--lunar .ap-shell{
    padding-top:clamp(70px,11vh,120px);
    padding-bottom:calc(100vw * 0.46);
  }
}
@media (prefers-reduced-motion:reduce){
  .ap-cta-canvas{transition:none}
}

/* The two sections below were the home page's alone and are shared with
   the service pages now, which is why they live here rather than in
   home.css. Their scripts moved the same way: assets/js/cases-map.js
   and assets/js/quotes.js. */

/* ---------- success stories · orbit arc ---------- */
/* The star map is edge to edge and left-aligned; the old arc was a centred
   block with its own top padding, which this section no longer has. */
/* z-index 4 clears the panda rail (sticky, z-index 3): once its own
   container ends, the panda hangs below it and would otherwise paint
   straight over the sky. */
.ap-stories{position:relative; z-index:4; padding:0; text-align:left; overflow:hidden}

/* nested wrapper carries the vertical fade, nesting multiplies the two masks */
/* The planet is a real moon shot (black background keyed out). The limb circle
   was least-squares fitted (cx 850, cy 1077, R 806 @1672px) and scaled so it
   tracks the 2300px orbit exactly: scale 1.4265. */
/* hover reveal: description + result */

/* ==========================================================================
   Success stories: the star map
   A fixed sky: every case has an immutable world coordinate and nothing here
   is dragged or zoomed by hand. Filtering frames the active constellation,
   opening a case reframes against the narrower pane, and the only thing that
   ever moves is the camera. home.js writes the geometry inline; this file
   owns the look and the one shared motion curve.
   ========================================================================== */
.ap-map{
  --cam:cubic-bezier(.22,.61,.36,1);
  position:relative; display:flex; width:100%;
  height:100vh; min-height:520px; max-height:1100px;
  overflow:hidden; background:var(--ink);
}
.ap-map-sky{position:relative; flex:1 1 auto; min-width:0; overflow:hidden}

/* oversized so the parallax can never pull an edge into frame */
.ap-map-bg{
  position:absolute; inset:-8%;
  background:var(--ink) url('../../uploads/case-map-bg.webp') center / cover no-repeat;
  transform:var(--bgtr,none);
  transition:transform 1s var(--cam);
}
.ap-map-veil{
  position:absolute; inset:0; pointer-events:none;
  background:
    radial-gradient(120% 90% at 18% 30%, rgba(255,90,20,.10), transparent 60%),
    radial-gradient(90% 70% at 70% 80%, rgba(70,100,255,.07), transparent 60%),
    /* fades to solid ink at the top and bottom edges, so the photograph
       never meets the sections above and below on a hard line */
    linear-gradient(180deg,
      var(--ink) 0%, rgba(3,3,5,.7) 9%, rgba(3,3,5,.32) 26%,
      rgba(3,3,5,.28) 64%, rgba(3,3,5,.72) 90%, var(--ink) 100%);
}
/* the drawn field sits on top of the photograph and adds the depth it lacks */
.ap-map-canvas{position:absolute; inset:0; width:100%; height:100%; opacity:.8; pointer-events:none}
.ap-map-grid{position:absolute; inset:0; width:100%; height:100%; pointer-events:none}
.ap-map-dust{position:absolute; inset:0; pointer-events:none}
.ap-map-dust i{position:absolute; border-radius:50%; background:#fff}

.ap-map-lines{position:absolute; inset:0; pointer-events:none}
.ap-map-line{
  position:absolute; left:0; top:0; height:1px; transform-origin:0 50%;
  background:var(--orange); width:100px; opacity:var(--lo,.42);
  transition:opacity .55s ease; will-change:transform;
}

.ap-map-names{position:absolute; inset:0; pointer-events:none}
.ap-map-cname{
  position:absolute; white-space:nowrap; left:0; top:0;
  display:var(--sd,none);
  font-family:'Space Mono',monospace; font-size:11px; letter-spacing:.22em;
  text-transform:uppercase; color:rgba(238,240,246,.34);
  transition:color .55s ease;
}

.ap-map-stars{position:absolute; inset:0}
/* No transition on position or size: home.js writes both every frame (the
   eased camera plus the drift), and a CSS transition on top would put the
   stars a frame behind the lines drawn from the same numbers. */
.ap-map-star{
  position:absolute; left:0; top:0;
  padding:0; border:0; background:none; color:inherit; cursor:pointer;
  opacity:var(--ao,1); pointer-events:var(--ape,auto);
  transition:opacity .55s ease; will-change:translate;
}
/* The photograph, untouched: no sheen, no scrim. The transparent border is
   the gap the ring needs, background-clip keeps the image inside the
   padding box, so the sky shows through the band between image and ring. */
.ap-map-disc{
  position:absolute; inset:0; border-radius:50%;
  background:#0c0d12 var(--aimg,none) center / cover no-repeat;
  background-clip:padding-box;
  border:var(--agap,5px) solid transparent;
  box-shadow:var(--aglow,none);
  transition:box-shadow .35s ease, border-width .3s ease;
}
/* the ring rides the border box, one gap out from the image */
.ap-map-disc::after{
  content:""; position:absolute; inset:calc(-1 * var(--agap,5px));
  border-radius:50%;
  border:var(--aringw,1px) solid var(--aringc,rgba(255,255,255,.45));
  transition:border-color .3s ease, border-width .3s ease;
}
.ap-map-ring{
  position:absolute; inset:-14px; border-radius:50%;
  border:1px solid var(--orange); display:var(--aring,none);
  animation:apStarPulse 3.4s ease-in-out infinite;
  pointer-events:none;
}
@keyframes apStarPulse{
  0%,100%{transform:scale(1); opacity:.55}
  50%{transform:scale(1.3); opacity:.1}
}
.ap-map-sname{
  position:absolute;
  top:var(--nt,50%); bottom:var(--nb,auto); left:var(--nl,auto); right:var(--nr,auto);
  transform:var(--ntr,translateY(-50%));
  font-size:var(--nfs,13px); font-weight:700; letter-spacing:.18em;
  text-transform:uppercase; white-space:nowrap; color:var(--nc,var(--paper));
  text-shadow:0 2px 12px rgba(0,0,0,.85);
  transition:color .3s ease, font-size .8s var(--cam);
}

/* ---- chrome ---- */
.ap-map-head{
  position:absolute; z-index:4;
  top:clamp(86px,11.5vh,116px); left:clamp(24px,3.4vw,56px); right:clamp(24px,3.4vw,56px);
  display:flex; flex-direction:column; align-items:center; gap:clamp(14px,2.4vh,24px);
  pointer-events:none;
}
.ap-map-head .ap-eyebrow{display:flex; align-items:center; gap:14px; text-indent:0}
.ap-map-head .ap-eyebrow i{display:block; width:46px; height:2px; background:var(--orange); flex:none}
.ap-map-head .ap-display{text-align:center; font-size:clamp(28px,3.2vw,52px); max-width:20ch}
.ap-map-filters{display:flex; flex-wrap:wrap; justify-content:center; gap:10px; pointer-events:auto}
.ap-map-filter{
  font-family:inherit; font-size:12px; font-weight:500; letter-spacing:.09em;
  text-transform:uppercase; padding:7px 15px; border-radius:999px; cursor:pointer;
  background:var(--fbg,rgba(255,255,255,.04));
  border:1px solid var(--fbd,rgba(238,240,246,.22));
  color:var(--fc,rgba(238,240,246,.75));
  transition:background .3s ease, border-color .3s ease, color .3s ease;
}
.ap-map-filter:hover{border-color:rgba(238,240,246,.5)}
.ap-map-filter.is-on{background:var(--orange); border-color:var(--orange); color:#120c07}
.ap-map-filter.is-on:hover{border-color:var(--orange)}

/* ---- the case panel ---- */
.ap-map-panel{
  position:relative; flex:0 0 auto; overflow:hidden;
  width:var(--asw,0px); opacity:var(--aso,0);
  border-left:1px solid var(--asb,transparent);
  background:linear-gradient(180deg,#0c0a0d,#08070a);
  transition:width .8s var(--cam), opacity .5s ease, border-color .5s ease;
}
.ap-map-panel-in{position:absolute; inset:0; display:flex; flex-direction:column; overflow:auto}
.ap-map-case{display:var(--pd,none); flex-direction:column; flex:1; min-height:0}
/* pushed below the site header, which floats over the whole section: at
   clamp(14px,...) the button sat under the navbar and could not be clicked */
.ap-map-close{
  position:absolute; z-index:3;
  top:clamp(96px,12vh,118px); right:clamp(18px,2.6vw,24px);
  width:34px; height:34px; border:0; background:none; cursor:pointer;
  color:var(--paper); font-size:20px; line-height:1; font-family:inherit;
  transition:color .25s ease;
}
.ap-map-close:hover{color:var(--orange)}
/* the top padding is what keeps the image clear of the close button */
/* The photograph fills its block edge to edge; the close button and the
   site header float over it, so a diagonal wash out of the top-right corner
   is what keeps the cross readable instead of a band of padding. */
.ap-map-shot{
  /* Takes whatever height the copy does not need, so a tall panel does not
     end in a band of empty background, but capped well short of the pane,
     because the copy under it now ends in a button and that button has to
     stay on screen. Everything here is relative (vh/%), never a fixed px. */
  position:relative; width:100%; flex:1 1 auto;
  min-height:clamp(150px,20vh,240px); max-height:42vh;
  background:#131015;
}
.ap-map-shot img{width:100%; height:100%; object-fit:cover; display:block}
.ap-map-shot::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(215deg,
    rgba(3,3,5,.92) 0%, rgba(3,3,5,.68) 22%, rgba(3,3,5,.28) 42%, rgba(3,3,5,0) 62%);
}
/* The copy follows the photograph directly. The handoff pins it to the
   bottom of the panel, which on a short screen opens a dead band under the
   image and then pushes the last line out of view. */
.ap-map-body{
  flex:none;
  padding:clamp(20px,3.4vh,40px) clamp(24px,3.4vw,56px) clamp(22px,3.4vh,38px);
  display:flex; flex-direction:column; align-items:flex-start; gap:clamp(10px,1.6vh,17px);
}
.ap-map-title{
  margin:0; font-family:'Anton',sans-serif; font-weight:400;
  font-size:clamp(26px,4.2vh,44px); line-height:1.05; letter-spacing:.005em;
  text-transform:uppercase; color:var(--paper); text-wrap:balance;
}
.ap-map-meta{
  font-family:'Space Mono',monospace; font-size:11.5px; letter-spacing:.18em;
  text-transform:uppercase; color:rgba(238,240,246,.62); line-height:1.5;
}
.ap-map-rule{width:52px; height:2px; background:var(--orange); flex:none}
.ap-map-text{
  margin:0; font-size:clamp(15px,1.9vh,18px); font-weight:300; line-height:1.5;
  color:var(--paper); max-width:460px; text-wrap:pretty;
}
.ap-map-stat{
  display:flex; align-items:baseline; gap:10px;
  font-family:'Space Mono',monospace; font-size:11px; letter-spacing:.14em;
  text-transform:uppercase; color:#9C9691;
}
.ap-map-stat b{font-family:'Anton',sans-serif; font-weight:400; font-size:26px; letter-spacing:.02em; color:var(--orange)}
.ap-map-cta{
  align-self:flex-start; margin-top:clamp(6px,1.4vh,14px);
  display:inline-flex; align-items:center; gap:11px;
  padding:clamp(11px,1.6vh,14px) clamp(20px,2vw,26px); border-radius:999px;
  background:var(--orange); color:#120c07; font-weight:500; font-size:15px;
  transition:background .25s ease;
}
/* pinned: the site-wide a:hover would otherwise paint the label orange on
   orange and it would read as the text vanishing */
.ap-map-cta:hover{background:#ff8a45; color:#120c07}
.ap-map-cta .ap-arw{transition:translate .25s ease}
.ap-map-cta:hover .ap-arw{translate:2px -2px}

/* Under 900px the sidebar cannot take 42% and leave a map worth looking at,
   so it becomes a sheet under the sky instead of a column beside it. */
@media (max-width:900px){
  .ap-map{flex-direction:column; height:auto; max-height:none}
  /* tall enough that the constellation still has room once the headline
     block has taken the top third */
  .ap-map-sky{flex:none; height:min(88vh,700px)}
  .ap-map-panel{width:auto !important; border-left:0; border-top:1px solid var(--line)}
  .ap-map-panel[hidden]{display:none}
  .ap-map-panel-in{position:static; overflow:visible}
  /* stacked: the header only floats over the sky, so the panel starts clean */
  .ap-map-close{top:clamp(14px,4vw,20px)}
  .ap-map-shot{flex:none; height:min(48vw,42vh); min-height:0; max-height:none}
  .ap-map-head{top:clamp(76px,9.5vh,96px)}
  /* A phone cannot carry eleven labels and eleven decorative names without
     turning into soup: the constellation keeps its photographs, and only the
     star you picked says its name. */
  .ap-map-names{display:none}
  .ap-map-sname{display:none}
  .ap-map-star.is-on .ap-map-sname{display:block}
  .ap-map-head .ap-display{font-size:clamp(24px,6.4vw,34px)}
}
@media (prefers-reduced-motion:reduce){
  .ap-map-star,.ap-map-line,.ap-map-cname,.ap-map-bg,.ap-map-panel{transition:none}
  .ap-map-ring{animation:none}
}

/* ---------- testimonials ----------
   One card, one quote at a time. The tabs along its top are the navigation
   and the auto-advance progress bar at once: the active one fills.
   ---------------------------------------------------------------------- */
.ap-quotes{
  position:relative; z-index:1; overflow:hidden;
  min-height:100vh; padding:clamp(70px,9vh,110px) 0;
  display:flex; flex-direction:column; justify-content:center;
}
.ap-quotes .ap-shell{
  display:flex; flex-direction:column; align-items:center; text-align:center;
  gap:clamp(18px,2.6vh,30px);
}
.ap-q-head{font-size:clamp(28px,3.4vw,54px); text-align:center; max-width:26ch}

.ap-q-lead{
  margin:0; max-width:56ch;
  font-size:clamp(15px,1.15vw,17.5px); font-weight:300; line-height:1.6; color:var(--muted);
  text-wrap:pretty;
}

/* the card */
.ap-q-card{
  position:relative; width:min(880px,100%);
  border:1px solid var(--line); border-radius:22px; overflow:hidden;
  background:
    radial-gradient(120% 90% at 12% 0%, rgba(255,90,20,.13), transparent 58%),
    radial-gradient(90% 80% at 92% 102%, rgba(255,90,20,.09), transparent 60%),
    linear-gradient(180deg,#0e0f15,#08090d);
  text-align:left;
}
.ap-q-tabs{display:flex; gap:10px; padding:clamp(18px,2.4vh,26px) clamp(20px,3vw,38px) 0}
.ap-q-tab{
  flex:1 1 0; height:2px; padding:0; border:0; border-radius:2px; cursor:pointer;
  background:rgba(238,240,246,.14); position:relative; overflow:hidden;
}
.ap-q-tab::after{
  content:""; position:absolute; inset:0 100% 0 0;
  background:var(--orange); transition:right .35s linear;
}
/* the fill runs for the dwell time, so the tab IS the countdown */
.ap-q-tab.is-on::after{right:0; transition:right var(--dwell,6s) linear}
.ap-q-tab.is-on.is-instant::after{transition:none}

/* pan-y so a vertical swipe still scrolls the page; the horizontal axis
   belongs to the carousel */
.ap-q-stage{
  position:relative; padding:clamp(22px,3.2vh,34px) clamp(20px,3vw,38px) clamp(18px,2.4vh,26px);
  touch-action:pan-y; cursor:grab;
  /* every slide sits in the same cell, so the stage is always as tall as the
     LONGEST quote and the card keeps one height from the first paint on.
     Absolute positioning the inactive ones took them out of the flow and the
     box jumped by a line or two on every change. */
  display:grid; align-items:start;
}
.ap-q-card.is-grab .ap-q-stage{cursor:grabbing; user-select:none}
.ap-q-slide{
  grid-area:1 / 1;
  display:grid; gap:clamp(14px,2vh,22px); align-content:start;
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .45s ease;
}
.ap-q-slide.is-on{opacity:1; visibility:visible; pointer-events:auto}

.ap-q-author{display:flex; align-items:center; gap:13px}
.ap-q-avatar{
  width:44px; height:44px; border-radius:50%; flex:none;
  display:grid; place-items:center;
  background:linear-gradient(150deg,var(--orange),#7d2f0b);
  font-family:'Space Mono',monospace; font-size:13px; color:#120c07;
}
.ap-q-author b{display:block; font-size:16px; font-weight:500; color:var(--paper)}
/* `b + span` and not `.ap-q-author span`: the looser selector also matched
   the avatar and knocked its grid centring out, dropping the initials above
   the circle */
.ap-q-author b + span{display:block; font-size:13px; font-weight:300; color:var(--muted)}
/* gold like the rating row below it: stars read as stars, not as brand */
.ap-q-stars{display:flex; gap:5px; color:#f2b31a}
.ap-q-text{
  margin:0; font-size:clamp(16px,1.35vw,20px); font-weight:300; line-height:1.6;
  color:rgba(238,240,246,.9); max-width:62ch; text-wrap:pretty;
}

.ap-q-foot{
  display:flex; align-items:center; gap:16px;
  font-family:'Space Mono',monospace; font-size:11.5px; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted);
}
/* gold, the one place on the page that is not the brand orange: a rating
   reads as a rating, and next to the orange it stays legible as one */
.ap-q-foot-stars{display:flex; gap:4px; color:#f2b31a}
/* inside the card, under the quote and hard right */
.ap-q-nav{
  display:flex; justify-content:flex-end; gap:10px;
  padding:0 clamp(20px,3vw,38px) clamp(20px,3vh,30px);
}
.ap-q-arrow{
  width:38px; height:38px; border-radius:50%; flex:none; cursor:pointer;
  display:grid; place-items:center;
  border:1px solid var(--line); background:none; color:var(--paper);
  font-size:15px; line-height:1; font-family:inherit;
  transition:border-color .25s ease, color .25s ease;
}
.ap-q-arrow:hover{border-color:var(--orange); color:var(--orange)}
.ap-q-foot-text b{font-family:'Anton',sans-serif; font-weight:400; font-size:17px; color:var(--paper); letter-spacing:.02em}

@media (max-width:760px){
  .ap-quotes{min-height:0; padding:70px 0 80px}
  .ap-q-card{border-radius:18px}
  /* the stage's padding computes to about 27px by 20px here, which on a
     390px screen is a tenth of the line each side */
  .ap-q-stage{padding:18px 14px 16px}
  .ap-q-slide{gap:12px}
  .ap-q-text{font-size:15.5px}
  .ap-q-foot{gap:10px; flex-wrap:wrap; justify-content:center}
}
@media (prefers-reduced-motion:reduce){
  .ap-q-slide{transition:none}
  .ap-q-tab::after,.ap-q-tab.is-on::after{transition:none}
  /* the sky on the menu cards stays, it just stops moving */
  .ap-nav-drop:hover .ap-mega-stars::before,
  .ap-nav-drop:hover .ap-mega-stars::after,
  .ap-nav-drop:focus-within .ap-mega-stars::before,
  .ap-nav-drop:focus-within .ap-mega-stars::after{animation:none}
  .ap-mega-stars::before,.ap-mega-stars::after{animation:none; opacity:.8}
}


/* ==========================================================================
   The WORK mega panel
   A list of specialities, the case we lead with, two more beside it. The
   speciality is a radio: hovering a label checks it and CSS shows the
   matching panel, so the swap is instant and works before any script has
   loaded. See includes/mega-work.php.
   ========================================================================== */
.ap-mega--work{padding:22px 26px 26px}
.ap-mw-state{position:absolute; opacity:0; pointer-events:none; width:0; height:0}

.ap-mw-grid{
  display:grid; grid-template-columns:240px minmax(0,1fr);
  gap:clamp(20px,2.4vw,44px); align-items:start;
}

/* ---- the specialities ---- */
.ap-mw-tabs{display:flex; flex-direction:column; gap:2px; padding-right:clamp(10px,1.6vw,26px)}
.ap-mw-tabs-head,.ap-mw-side-head{
  font-family:'Space Mono',monospace; font-size:10px; letter-spacing:.24em;
  text-transform:uppercase; color:rgba(238,240,246,.45); margin-bottom:14px;
}
.ap-mw-all,.ap-mw-tab{
  position:relative;
  display:flex; align-items:center; gap:8px;
  padding:11px 0; font-size:15.5px; font-weight:400; color:rgba(238,240,246,.8);
  border-bottom:1px solid transparent; cursor:pointer;
  transition:color .2s ease, border-color .2s ease;
}
/* A hairline between one speciality and the next, drawn as a top border
   on each of them: that never collides with the bottom border the active
   one wears, and it leaves no line hanging under the last item. It spans
   the column, not the label, whose width changes with the word. */
.ap-mw-tab{border-top:1px solid rgba(238,240,246,.08); width:100%; max-width:200px}
.ap-mw-all{width:100%; max-width:200px}
.ap-mw-all{color:var(--paper); text-decoration:none; margin-bottom:4px}
.ap-mw-all i{display:inline-flex; font-size:11px; color:var(--orange)}
.ap-mw-all:hover,.ap-mw-tab:hover{color:var(--paper)}

/* the checked speciality is the one the panel is showing */
#ap-mw-web:checked       ~ .ap-mw-grid .ap-mw-tab[data-tab="web"],
#ap-mw-software:checked  ~ .ap-mw-grid .ap-mw-tab[data-tab="software"],
#ap-mw-ecommerce:checked ~ .ap-mw-grid .ap-mw-tab[data-tab="ecommerce"],
#ap-mw-brand:checked     ~ .ap-mw-grid .ap-mw-tab[data-tab="brand"]{
  color:var(--orange); border-bottom-color:var(--orange);
}
/* the next tab's hairline is painted over the orange one, so it stands
   down while its neighbour is the active speciality */
#ap-mw-web:checked       ~ .ap-mw-grid .ap-mw-tab[data-tab="web"] + .ap-mw-tab,
#ap-mw-software:checked  ~ .ap-mw-grid .ap-mw-tab[data-tab="software"] + .ap-mw-tab,
#ap-mw-ecommerce:checked ~ .ap-mw-grid .ap-mw-tab[data-tab="ecommerce"] + .ap-mw-tab{
  border-top-color:transparent;
}
/* a keyboard user lands on the label, so it has to show it */
.ap-mw-tab:focus-visible,.ap-mw-state:focus-visible + label{outline:2px solid var(--orange); outline-offset:3px}

/* ---- which panel is on ---- */
.ap-mw-panel{display:none}
#ap-mw-all:checked       ~ .ap-mw-grid .ap-mw-panel[data-panel="all"],
#ap-mw-web:checked       ~ .ap-mw-grid .ap-mw-panel[data-panel="web"],
#ap-mw-software:checked  ~ .ap-mw-grid .ap-mw-panel[data-panel="software"],
#ap-mw-ecommerce:checked ~ .ap-mw-grid .ap-mw-panel[data-panel="ecommerce"],
#ap-mw-brand:checked     ~ .ap-mw-grid .ap-mw-panel[data-panel="brand"]{
  display:grid; grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);
  gap:clamp(20px,2.6vw,46px); align-items:start;
}

/* ---- the case we lead with ----
   The /work/ feature at menu size: copy over the artwork with a scrim
   under it. Stacking the two made the panel a screen and a half tall. */
.ap-mw-lead{
  position:relative; display:flex; align-items:flex-end; overflow:hidden;
  aspect-ratio:16/9; border-radius:14px; border:1px solid var(--line);
  background:#07080c; text-decoration:none; color:inherit;
  transition:border-color .3s ease;
}
.ap-mw-lead:hover{border-color:rgba(255,90,20,.5)}
.ap-mw-lead-art{position:absolute; inset:0; display:block; overflow:hidden}
.ap-mw-lead-art img{
  width:100%; height:100%; object-fit:cover; display:block;
  transition:scale .6s cubic-bezier(.2,.7,.3,1);
}
.ap-mw-lead:hover .ap-mw-lead-art img{scale:1.04}
.ap-mw-lead-art::after{
  content:""; position:absolute; inset:0;
  background:
    linear-gradient(180deg,rgba(3,3,5,.2) 0%,rgba(3,3,5,.05) 30%,rgba(3,3,5,.86) 80%,rgba(3,3,5,.95) 100%),
    linear-gradient(90deg,rgba(3,3,5,.82) 0%,rgba(3,3,5,.25) 52%,transparent 76%);
}
.ap-mw-lead-body{
  position:relative; z-index:1; display:block;
  padding:clamp(18px,2vw,30px); max-width:min(560px,78%);
}
.ap-mw-lead-name{
  display:block; font-family:'Anton',sans-serif; font-weight:400;
  font-size:clamp(24px,2.4vw,38px); line-height:1.02; letter-spacing:.01em;
  text-transform:uppercase; color:var(--paper);
}
.ap-mw-meta{
  display:block; margin-top:7px; font-family:'Space Mono',monospace;
  font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
}
.ap-mw-lead-desc{display:block; margin-top:10px; font-size:14.5px; font-weight:300; color:rgba(238,240,246,.82); max-width:44ch}
.ap-mw-lead-btn{margin-top:16px; padding:11px 20px; font-size:12px}
.ap-mw-lead:hover .ap-mw-lead-btn{background:var(--orange); border-color:var(--orange); color:#120c07}
.ap-mw-lead:hover .ap-mw-lead-btn span{color:#120c07}

/* ---- the two beside it ---- */
.ap-mw-side{
  display:flex; flex-direction:column; gap:16px; height:100%;
  border-left:1px solid var(--line); padding-left:clamp(16px,2vw,34px);
}
.ap-mw-mini{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:16px; align-items:center; text-decoration:none; color:inherit}
.ap-mw-mini-art{
  display:block; overflow:hidden; border-radius:10px; aspect-ratio:16/10;
  border:1px solid var(--line); background:#07080c;
}
.ap-mw-mini-art img{width:100%; height:100%; object-fit:cover; display:block; transition:scale .5s cubic-bezier(.2,.7,.3,1)}
.ap-mw-mini:hover .ap-mw-mini-art{border-color:rgba(255,90,20,.5)}
.ap-mw-mini:hover .ap-mw-mini-art img{scale:1.05}
.ap-mw-mini-text strong{
  display:block; font-family:'Anton',sans-serif; font-weight:400; font-size:19px;
  line-height:1.1; text-transform:uppercase; color:var(--paper);
}
.ap-mw-mini-arrow{display:inline-flex; margin-top:10px; font-size:13px; color:var(--orange); transition:translate .25s ease}
.ap-mw-mini:hover .ap-mw-mini-arrow{translate:3px 0}
/* A case with no page of its own: same wording and same quiet as the
   listing's card, and the art stops lighting up on hover so the row does
   not behave like something that leads anywhere new. */
.ap-mw-mini-soon{
  display:inline-flex; margin-top:10px;
  font-family:'Space Mono',monospace; font-size:10px;
  letter-spacing:.14em; text-transform:uppercase; color:rgba(238,240,246,.38);
}
.ap-mw-mini.is-soon:hover .ap-mw-mini-art{border-color:var(--line)}
.ap-mw-mini.is-soon:hover .ap-mw-mini-art img{scale:1}

/* the way out of the menu, at the foot of the column it belongs to */
.ap-mw-side-cta{margin-top:auto; align-self:flex-end; padding:11px 20px; font-size:12px}

@media (max-width:1100px){
  .ap-mw-grid{grid-template-columns:200px minmax(0,1fr)}
  #ap-mw-all:checked       ~ .ap-mw-grid .ap-mw-panel[data-panel="all"],
  #ap-mw-web:checked       ~ .ap-mw-grid .ap-mw-panel[data-panel="web"],
  #ap-mw-software:checked  ~ .ap-mw-grid .ap-mw-panel[data-panel="software"],
  #ap-mw-ecommerce:checked ~ .ap-mw-grid .ap-mw-panel[data-panel="ecommerce"],
  #ap-mw-brand:checked     ~ .ap-mw-grid .ap-mw-panel[data-panel="brand"]{grid-template-columns:minmax(0,1fr)}
  .ap-mw-side{border-left:0; padding-left:0; border-top:1px solid var(--line); padding-top:18px}
  .ap-mw-mini{grid-template-columns:120px minmax(0,1fr)}
}

/* ==========================================================================
   The work card
   Artwork on top, copy underneath, a button at the foot. Used by the /work/
   listing, the services hub and a case's "more work" row, so it lives here
   rather than in any one of their stylesheets. A row of them is always one
   height: the grid stretches, the body takes the slack so the buttons line
   up, and a description is capped at three lines.
   ========================================================================== */
.ap-work-card{
  position:relative; display:flex; flex-direction:column; overflow:hidden;
  border:1px solid var(--line); border-radius:16px; background:#0a0b10;
  transition:border-color .35s ease, translate .35s ease, background .35s ease;
}
.ap-work-card:hover{border-color:var(--orange); translate:0 -6px; background:#0e1016}
/* the whole card is the anchor, so it has to behave like one */
.ap-work-card.is-link{text-decoration:none; color:inherit}
.ap-work-card.is-link:focus-visible{outline:2px solid var(--orange); outline-offset:3px}
/* the CTA is a span inside that anchor now: it still lights up with the card */
.ap-work-card.is-link:hover .ap-work-cta{color:var(--orange)}

.ap-work-media{display:block; overflow:hidden; aspect-ratio:16/10; background:#07080c}
.ap-work-card--pair .ap-work-media{aspect-ratio:16/9}
.ap-work-media img{
  width:100%; height:100%; object-fit:cover; object-position:center; display:block;
  transition:scale .6s cubic-bezier(.2,.7,.3,1);
}
.ap-work-card:hover .ap-work-media img{scale:1.04}

.ap-work-body{
  display:flex; flex-direction:column; align-items:flex-start; gap:7px;
  padding:22px 22px 24px;
  /* takes the slack, so the button lands on the same line across a row
     whether the description ran to one line or three */
  flex:1 1 auto;
}
.ap-work-name{
  margin:0; font-family:'Anton',sans-serif; font-weight:400; letter-spacing:.01em;
  text-transform:uppercase; line-height:1; font-size:23px; color:var(--paper);
}
.ap-work-card--pair .ap-work-name{font-size:27px}
.ap-work-meta{
  margin:0; font-family:'Space Mono',monospace; font-size:10.5px;
  letter-spacing:.16em; text-transform:uppercase; color:var(--muted);
}
.ap-work-desc{
  margin:4px 0 0; font-size:14.5px; font-weight:300; line-height:1.5;
  color:var(--muted); text-wrap:pretty;
  /* three lines at most: past that one long description drags its whole
     row taller than every card in it */
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:3; overflow:hidden;
}
/* a real button, not a link that looks like one: this is the card's action */
.ap-work-cta{
  margin-top:auto; padding-top:14px; display:inline-flex; align-items:center; gap:8px;
  padding:9px 17px; border-radius:999px; text-decoration:none;
  border:1px solid rgba(255,90,20,.45); color:var(--orange);
  font-size:13px; letter-spacing:.02em;
  transition:background .25s ease, border-color .25s ease, color .25s ease;
}
.ap-work-cta:hover{background:var(--orange); border-color:var(--orange); color:#120c07}
.ap-work-cta span{display:inline-flex; transition:translate .25s ease}
.ap-work-cta:hover span{translate:2px -2px}
.ap-work-soon{
  margin-top:auto; padding-top:16px; font-family:'Space Mono',monospace; font-size:10.5px;
  letter-spacing:.14em; text-transform:uppercase; color:rgba(238,240,246,.38);
}

.ap-work-card[hidden]{display:none}
.ap-work-empty{
  margin:clamp(30px,6vh,60px) 0 0; text-align:center;
  font-size:16px; font-weight:300; color:var(--muted);
}

/* ==========================================================================
   Edge fades for sections whose artwork runs to the edge
   A black overlay only moves the seam: the next section has its own
   background, so fading artwork to a COLOUR still meets that background on a
   line. These fade the section to ALPHA instead, and the page's own ground
   carries straight through the join.
   Percentages, so the band scales with the section rather than with the
   viewport, and --fade-top:0% means "nothing above me to blend into".
   ========================================================================== */
.ap-svc-hero,
.ap-cta-band--lunar{
  --fade-top:0%; --fade-bottom:7%;
  -webkit-mask-image:linear-gradient(180deg,
    transparent 0, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)), transparent 100%);
  mask-image:linear-gradient(180deg,
    transparent 0, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)), transparent 100%);
}
/* Success stories is the exception: instead of dissolving it is framed,
   with the same hairline the rest of the site divides things with. A fade
   AND a rule would read as the artwork failing to reach its own border. */
.ap-stories{border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
.ap-svc-hero{--fade-bottom:6%}
/* the lunar surface is bright right up to the edge, so it needs longer */
.ap-cta-band--lunar{--fade-bottom:13%}

/* ==========================================================================
   Stack & tools

   Here rather than in service.css because includes/stack.php is shared:
   the service pages, the case pages and the industry pages all include
   it, and each of those loads a different page stylesheet. Split across
   two of them, a case page had the grid variant and none of the rail it
   overrides, and an industry page had the rail and none of the grid, so
   its single copy of the list kept sliding off the edge and snapping
   back. One partial, one home.
   ========================================================================== */
.ap-stack{overflow:hidden}
.ap-stack-head{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:40px; flex-wrap:wrap; margin-bottom:clamp(54px,7.4vh,96px);
}
.ap-stack .ap-eyebrow{display:flex; align-items:center; gap:14px}
.ap-stack .ap-eyebrow i{display:block; width:46px; height:2px; background:var(--orange); flex:none}
.ap-stack-lead{
  margin:0 0 6px; max-width:32ch;
  font-size:16px; font-weight:300; line-height:1.5; color:var(--muted);
}

/* full bleed, and faded at both ends so the loop has no visible seam */
.ap-stack-rails{
  width:100vw; margin-left:calc(50% - 50vw);
  display:flex; flex-direction:column; gap:14px;
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 9%,#000 91%,transparent 100%);
  mask-image:linear-gradient(90deg,transparent 0,#000 9%,#000 91%,transparent 100%);
}
/* Scrollable rather than clipped: the marquee shows the list, the drag is
   how you actually reach the end of it. The scrollbar is hidden because the
   rail is decorative chrome, and .is-held is set by global.js on the first
   touch, which stops the marquee for good. */
.ap-stack-rail{
  overflow-x:auto; overflow-y:hidden;
  scrollbar-width:none; -ms-overflow-style:none;
  overscroll-behavior-x:contain;
  touch-action:pan-x pan-y;
  cursor:grab;
}
.ap-stack-rail::-webkit-scrollbar{display:none}
.ap-stack-rail.is-held{cursor:grabbing}
.ap-stack-rail.is-held .ap-stack-track{animation-play-state:paused !important}
/* The track holds an even number of copies of the list and travels
   exactly half its own width, so the moment it resets nothing has
   visibly moved. --copies comes from the partial, which prints as many
   as the list needs to outrun the viewport; the duration follows it so
   the rail keeps one speed whatever that count is. */
.ap-stack-track{
  display:flex; width:max-content;
  animation:apStackRun calc(var(--copies, 2) * 26s) linear infinite;
}
.ap-stack-rails:hover .ap-stack-track{animation-play-state:paused}
@keyframes apStackRun{ from{transform:translateX(0)} to{transform:translateX(-50%)} }

/* No pill: the marks sit straight on the sky, spaced far enough apart to
   read as a row of logos rather than a row of buttons. */
.ap-stack-set{
  display:flex; align-items:center; gap:clamp(40px,4.4vw,74px);
  list-style:none; margin:0; padding:0 clamp(20px,2.2vw,37px);
}
.ap-stack-chip{
  flex:none; white-space:nowrap; display:flex; align-items:center;
  font-size:21px; font-weight:400; letter-spacing:.01em;
  color:rgba(238,240,246,.72);
  transition:color .3s ease;
}
.ap-stack-chip:hover{color:var(--orange)}

/* Logos are forced to flat white rather than shown in brand colour. Half of
   these ship as black-on-transparent and would be invisible here, and a rail
   of a dozen different brand palettes fights everything around it. The
   filter turns any input, colour or black, into the same white mark. */
.ap-stack-chip--logo img{
  display:block; height:34px; width:auto; max-width:230px;
  filter:brightness(0) invert(1);
  opacity:.72; transition:opacity .3s ease;
}
.ap-stack-chip--logo:hover img{opacity:1}

/* centred under the rail, and well clear of it: the marquee needs air on
   both sides or the three blocks read as one stack */
.ap-stack-cta{display:flex; justify-content:center; margin-top:clamp(52px,7vh,92px)}

@media (max-width:760px){
  .ap-stack-chip{font-size:17px}
  .ap-stack-chip--logo img{height:26px; max-width:168px}
  .ap-stack-rails{gap:10px}
}
@media (prefers-reduced-motion:reduce){
  /* no drift: it becomes a plain scrollable row */
  .ap-stack-track{animation:none}
  .ap-stack-rail{overflow-x:auto; scrollbar-width:none}
  .ap-stack-rail::-webkit-scrollbar{display:none}
  /* every copy past the first: the count is not fixed any more */
  .ap-stack-set:not(:first-child){display:none}
}


/* ---------- the stack, as a grid ----------
   This lived in case.css, which meant the class did nothing on any
   other page that set $stack_layout = 'grid': one copy of the list
   with the marquee still running, sliding it off the edge and
   snapping back. It styles a shared partial, so it belongs beside
   the rail it replaces. */
.ap-stack--grid .ap-stack-rails{
  width:auto; margin-left:0;
  -webkit-mask-image:none; mask-image:none;
}
.ap-stack--grid .ap-stack-rail{overflow:visible}
.ap-stack--grid .ap-stack-track{animation:none; width:auto}
/* Flex, not a grid of tracks: a case names five or six tools, and fixed
   columns left one of them stranded alone on a second row. Wrapping and
   centring keeps any count balanced. */
.ap-stack--grid .ap-stack-set{
  display:flex; flex-wrap:wrap; justify-content:center; align-items:center;
  gap:clamp(26px,3.4vw,52px) clamp(30px,4vw,72px);
  width:min(var(--shell),100% - 48px); margin-inline:auto;
  background:none; border:0;
}
.ap-stack--grid .ap-stack-chip{
  flex:0 0 auto; margin:0; padding:6px; min-height:56px;
  white-space:normal; text-align:center; font-size:17px; line-height:1.3;
  display:grid; place-items:center; background:none; border:0;
  /* irregular on purpose: --dur and --delay are set per position below, so
     no two neighbours rise together */
  animation:apStackFloat var(--dur,7.4s) ease-in-out var(--delay,0s) infinite;
}
.ap-stack--grid .ap-stack-chip img{max-height:42px; width:auto}
@keyframes apStackFloat{0%,100%{translate:0 0}50%{translate:0 var(--rise,-10px)}}
/* seven steps, so the pattern does not line up with a row of four, five or
   six columns and the drift never looks like a wave */
.ap-stack--grid .ap-stack-chip:nth-child(7n+1){--dur:7.2s;  --delay:0s;    --rise:-11px}
.ap-stack--grid .ap-stack-chip:nth-child(7n+2){--dur:8.6s;  --delay:-2.1s; --rise:-7px}
.ap-stack--grid .ap-stack-chip:nth-child(7n+3){--dur:6.4s;  --delay:-1.2s; --rise:-13px}
.ap-stack--grid .ap-stack-chip:nth-child(7n+4){--dur:9.3s;  --delay:-3.4s; --rise:-8px}
.ap-stack--grid .ap-stack-chip:nth-child(7n+5){--dur:7.8s;  --delay:-0.6s; --rise:-14px}
.ap-stack--grid .ap-stack-chip:nth-child(7n+6){--dur:8.1s;  --delay:-4.2s; --rise:-9px}
.ap-stack--grid .ap-stack-chip:nth-child(7n+7){--dur:6.9s;  --delay:-2.8s; --rise:-12px}

@media (prefers-reduced-motion:reduce){
  .ap-stack--grid .ap-stack-chip{animation:none}
}


/* ==========================================================================
   How we work: an irregular track from one planet to the next
   ========================================================================== */
.ap-proc{
  /* Its own containing block. The planets are absolute and the section
     clips them, and both only work if the section is the positioned
     ancestor; it used to inherit that from .ap-svc-section, which lives in
     service.css and is not loaded on /contact/. */
  position:relative;
  overflow:hidden;
  min-height:100vh; padding:110px 0;
  display:flex; flex-direction:column; justify-content:center;
}
.ap-proc .ap-shell{position:relative; z-index:2}
/* Planets that bleed past the bottom of a section end on a hard horizontal
   cut; this washes the last stretch back into the page. Shared by the web
   design hero and by How we work, so both cuts disappear the same way. */
.ap-bottom-fade{
  position:absolute; left:0; right:0; bottom:0; z-index:5;
  height:clamp(110px,20vh,240px); pointer-events:none;
  background:linear-gradient(180deg,rgba(3,3,5,0) 0%,rgba(3,3,5,.72) 52%,var(--ink) 100%);
}

.ap-proc-planet{position:absolute; z-index:4; pointer-events:none; max-width:none}
/* The origin: the small moon, pinned to the first point of the path
   (35,116 of the 1000x220 space), so the line leaves from its middle.
   Mirrored, like its big counterpart, to face the track it feeds. */
.ap-proc-planet--from{
  left:3.5%; top:116px; translate:-50% -50%; scale:-1 1;
  width:clamp(130px,12vw,210px);
  opacity:.95;
}
/* The destination: a lit limb crossing the lower-right corner, mostly
   off-screen. Mirrored so the lit edge faces the incoming line; the mask is
   authored in the element's own space, so the flip carries it to the left
   side for free. Offset and fade are both fractions of the planet's own
   width, so the same slice of limb shows at every viewport instead of
   shrinking to a sliver on the narrow ones: 42% of the disc is on screen and
   the last third of that fades out before it reaches the last column. */
.ap-proc-planet--to{
  --w:clamp(420px,39vw,700px);
  width:var(--w); right:calc(var(--w) * -.425); bottom:-24%;
  scale:-1 1;
  -webkit-mask-image:linear-gradient(90deg,#000 70%,transparent 88%);
  mask-image:linear-gradient(90deg,#000 70%,transparent 88%);
}

.ap-proc-head{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:40px; flex-wrap:wrap; margin-bottom:26px;
}
.ap-proc-headings{max-width:720px}
/* the eyebrow's leading rule: this section is a journey, and the dash reads
   as the start of the track it introduces */
.ap-proc-eyebrow{display:flex; align-items:center; gap:14px}
.ap-proc-eyebrow i{display:block; width:46px; height:2px; background:var(--orange); flex:none}
.ap-proc-lead{
  margin:0 0 6px; max-width:30ch;
  font-size:16px; font-weight:300; line-height:1.5; color:var(--muted);
}

/* Track. Full bleed: the curve has to run out to both planets, which sit
   against the viewport edges, not against the shell. Height is fixed so the
   wave keeps its amplitude in px while the curve stretches horizontally. */
.ap-proc-track{
  position:relative; height:330px; margin-top:52px;
  width:100vw; margin-left:calc(50% - 50vw);
}
.ap-proc-line-wrap{position:absolute; left:0; top:0; width:100%; height:220px}
/* .ap-rv would translate the wrapper away from the dots while it fades in, so
   the line only borrows the reveal's opacity and wipes itself in instead. */
.ap-proc .ap-proc-line-wrap{transform:none}
.ap-proc-line{
  position:absolute; inset:0; width:100%; height:100%; overflow:visible;
  clip-path:inset(0 100% 0 0); /* near-linear on purpose: a front-loaded curve races ahead of the steps */
  transition:clip-path 2.9s cubic-bezier(.4,.1,.5,.95);
}
.ap-proc-line-wrap.is-in .ap-proc-line{clip-path:inset(0 0 0 0)}

.ap-proc-steps{list-style:none; margin:0; padding:0}
.ap-proc-step{
  position:absolute; left:var(--x); top:calc(var(--y) * 1px);
  /* centred with the `translate` property, not `transform`: the .ap-rv reveal
     animates transform and would otherwise wipe the centring out. */
  translate:-50% 0;
  /* Nodes sit (100 / (steps + 1))% apart, so the column is that gap less a
     real gutter. Hard-coding 13% was right for five steps and left three
     looking like five with holes in it. */
  width:min(calc(100% / (var(--steps,5) + 1) - 2.5%),280px);
  display:flex; flex-direction:column; align-items:center; text-align:center;
}
.ap-proc-marker{position:relative; display:block; width:100%; height:0}
.ap-proc-num{
  position:absolute; left:50%; bottom:22px; transform:translateX(-50%);
  font-family:'Space Mono',monospace; font-size:15px; letter-spacing:.14em;
  color:rgba(238,240,246,.72); font-variant-numeric:tabular-nums;
}
.ap-proc-dot{
  position:absolute; left:50%; top:0; transform:translate(-50%,-50%);
  width:13px; height:13px; border-radius:50%;
  background:#ffd9b8;
  box-shadow:0 0 16px 5px rgba(255,110,30,.75), 0 0 4px 1px rgba(255,200,150,.95);
}
.ap-proc-title{
  margin-top:34px;
  font-size:19px; font-weight:500; letter-spacing:.01em; color:var(--paper);
}
.ap-proc-desc{
  margin-top:8px;
  font-size:14.5px; font-weight:300; line-height:1.5; color:var(--muted);
  text-wrap:pretty;
}

/* Each step waits for the line to reach it: the wipe runs 2.9s over the full
   width, and the nodes sit at 19/34.5/50/65.5/81%, so ~.42s a step keeps the
   two roughly together without syncing them frame by frame. The steps also
   fade slower than the site default, to match. !important because the shared
   reveal observer writes its own generic 90ms stagger inline. */
.ap-proc-step.ap-rv{
  transition-duration:1.1s;
  transition-delay:calc(.45s + var(--i) * .42s) !important;
}

/* Stacked layout: the curve is meaningless in one column, so it goes and the
   steps become a plain numbered list with the track as a left rail. Below
   ~1040px the five columns get too narrow to read before they get too short. */
@media (max-width:1040px){
  .ap-proc-line-wrap,.ap-proc-planet--from{display:none}
  .ap-proc{min-height:0; padding:90px 0; display:block}
  .ap-proc-track{height:auto; width:auto; margin-left:0}
  .ap-proc-step.ap-rv{transition-duration:.7s; transition-delay:calc(var(--i) * .16s) !important}
  /* nudged in so the dots' glow is not cut off by the viewport edge */
  .ap-proc-steps{display:flex; flex-direction:column; gap:34px; margin-left:14px; padding-left:34px; border-left:1px solid var(--line)}
  .ap-proc-step{
    position:static; translate:none; width:auto;
    align-items:flex-start; text-align:left;
  }
  .ap-proc-marker{height:auto; display:flex; align-items:center; gap:12px; width:auto}
  .ap-proc-num{position:static; transform:none}
  /* -34px == the list's padding-left, which puts the dot on the rail */
  .ap-proc-dot{position:absolute; left:-34px; top:50%}
  .ap-proc-title{margin-top:6px}
  .ap-proc-planet--to{--w:300px; bottom:-8%; opacity:.6}
}

@media (prefers-reduced-motion:reduce){
  .ap-proc-line{clip-path:none; transition:none}
  .ap-proc-step.ap-rv{transition-duration:0s; transition-delay:0s !important}
}


/* The same track at the foot of /contact/: the steps are what happens after
   the button, so the section follows the form rather than owning a screen of
   its own. Only the framing changes; the curve and the planets are shared. */
.ap-proc--next{min-height:0; padding:0 0 clamp(90px,13vh,160px)}
.ap-proc--next .ap-proc-track{margin-top:30px}
/* The section is a third the height, so the limb is scaled to match: at the
   service pages' size it filled the whole right half of it. */
.ap-proc--next .ap-proc-planet--to{--w:clamp(300px,27vw,470px); bottom:-16%}   /* high enough that the line still lands on the limb */
.ap-proc--next .ap-proc-planet--from{width:clamp(96px,8vw,150px)}
@media (max-width:1040px){
  .ap-proc--next{padding-bottom:clamp(70px,10vh,120px)}
  /* Stacked, the steps run down the left and the limb was sitting behind
     the last one's copy. Same retreat the base section makes. */
  .ap-proc--next .ap-proc-planet--to{--w:260px; bottom:-6%; opacity:.5}
}

/* ---------------- animation budget ----------------
   .ap-idle is set by global.js on a decoration that is off screen, and
   .ap-still on <html> while the tab is hidden or the window is in the
   background. Both only ever pause an animation that is already playing,
   so nothing moves position and nothing reflows: the element holds the
   frame it was on and resumes from there. */
.ap-idle, .ap-idle *,
.ap-still:not(.ap-boot) .ap-header,
.ap-still:not(.ap-boot) .ap-planet,
.ap-still:not(.ap-boot) .ap-b-planet,
.ap-still:not(.ap-boot) .ap-b-planet img,
.ap-still:not(.ap-boot) .ap-halo-dot,
.ap-still:not(.ap-boot) .ap-panda-inner,
.ap-still:not(.ap-boot) .ap-hint,
.ap-still:not(.ap-boot) .ap-side-cue em,
.ap-still:not(.ap-boot) .ap-stack-run,
.ap-still:not(.ap-boot) .ap-stack-tile,
.ap-still:not(.ap-boot) .ap-mega-stars span{
  animation-play-state:paused !important;
}

/* The intro is the one animation that must never be paused half-played:
   its elements start at opacity 0 and the animation is what reveals them,
   so freezing it on an unfocused window would leave the hero blank. */
.ap-boot .ap-b-bg, .ap-boot .ap-b-head, .ap-boot .ap-b-nav,
.ap-boot .ap-b-panda, .ap-boot .ap-b-foot, .ap-boot .ap-b-planet,
.ap-boot .ap-side-cue{
  animation-play-state:running !important;
}
