/* ==========================================================
   JAGA24 — Premium landing
   Stack: Vanilla CSS only
   Theme: off-black + signal green (single accent)
   ========================================================== */

/* ---------- Tokens ---------- */
:root {
  /* Follow the dark theme for native scrollbars + form controls */
  color-scheme: dark;

  /* Off-black base, never pure #000 */
  --bg:        #0a0a0c;
  --bg-soft:   #0f1012;
  --bg-soft2:  #14151a;
  --line:      rgba(255, 255, 255, 0.07);
  --line-2:    rgba(255, 255, 255, 0.12);

  /* Text — tinted, single warm-neutral family.
     Mute/faint alpha raised so the 11-13px mono labels that use them clear
     WCAG AA (4.5:1) against --bg. */
  --ink:       #ecede8;
  --ink-soft:  rgba(236, 237, 232, 0.72);
  --ink-mute:  rgba(236, 237, 232, 0.60);
  --ink-faint: rgba(236, 237, 232, 0.45);

  /* Single accent — signal green, desaturated */
  --acc:       #6eff8b;
  --acc-glow:  rgba(110, 255, 139, 0.18);

  /* Fonts */
  --f-sans:    'Geist', system-ui, -apple-system, sans-serif;
  --f-serif:   'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  --f-mono:    'JetBrains Mono', ui-monospace, monospace;

  /* Layout */
  --rad:       14px;
  --rad-sm:    8px;
  --rad-lg:    22px;
  --max:       1280px;
  --pad:       clamp(20px, 4vw, 56px);
  --section:   clamp(80px, 12vw, 160px);

  /* Mobile dock tab bar. Single source of truth so the bar height, the WhatsApp
     lift and the footer pad all move together — see the dock section below.
     `env(safe-area-inset-bottom)` matters here: the viewport meta sets
     `viewport-fit=cover`, so without it the bar would sit under the iOS home
     indicator. Always resolves to 0 where there is no inset. */
  --dock-h:    62px;
  --dock-safe: calc(var(--dock-h) + env(safe-area-inset-bottom));

  /* Motion */
  --ease:      cubic-bezier(.2, .8, .2, 1);
  --ease-out:  cubic-bezier(.16, 1, .3, 1);
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body, h1, h2, h3, h4, p, ul, ol, figure, blockquote, form { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
input, textarea, button { font: inherit; }
:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; border-radius: 4px; }

/* ---------- Base ---------- */
html, body { background: var(--bg); color: var(--ink); }
body {
  font-family: var(--f-sans);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.55;
  font-feature-settings: "ss01", "cv11", "cv02";
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* `clip` rather than `hidden`: `hidden` turns <body> into a scroll container,
     which fights ScrollSmoother's native-scroll model (the plugin sizes <body>
     itself to produce the scrollbar). `clip` still stops horizontal overflow. */
  overflow-x: clip;
}

::selection { background: var(--acc); color: #0a0a0c; }

em { font-family: var(--f-serif); font-style: italic; font-weight: 400; letter-spacing: -0.01em; }
strong { font-weight: 600; }

/* ---------- Skip link ---------- */
.skip-link {
  position: absolute; top: -100px; left: 16px;
  background: var(--acc); color: #0a0a0c;
  padding: 10px 16px; border-radius: 6px; font-weight: 600;
  z-index: 9999; transition: top .2s var(--ease);
}
.skip-link:focus { top: 16px; }

/* ---------- Grain overlay ---------- */
.grain {
  position: fixed; inset: 0; pointer-events: none; z-index: 9000;
  opacity: .06; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

/* ---------- Reveal animation (scroll-driven) ----------
   Scoped to `.js`, which the inline <head> script adds — so this only protects
   against scripting being disabled. If main.js itself fails, the `.js-fallback`
   rule further down restores visibility (see the inline script in index.html). */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .9s var(--ease-out), transform .9s var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-in {
  opacity: 1;
  transform: translateY(0);
}
/* Set on <html> by main.js once the GSAP scrub tweens own the reveal, paired
   with `.is-scrubbed` on each element GSAP actually drives. A scrubbed tween
   writes opacity/transform every frame, so the transition above would lag a
   frame behind the scroll — and `--reveal-delay` would fight the ScrollTrigger
   start. Scoping to `.is-scrubbed` matters: the hero children are deliberately
   NOT scrubbed and still need that transition for their one-shot fade-up.
   Specificity (0,3,1) beats `.js [data-reveal]` (0,2,0), no !important needed;
   `transition: none` also clears the delay, so `--reveal-delay` is inert here. */
html.js-scrub [data-reveal].is-scrubbed { transition: none; }
/* Set by the inline <head> script when js/main.js never announces itself
   (blocked CDN, 404, parse error). Without this the whole page below the hero
   would sit at opacity:0 forever. `html` raises specificity above `.js …`. */
html.js-fallback [data-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 22px; border-radius: 999px;
  font-weight: 500; font-size: 14px; letter-spacing: -0.005em;
  white-space: nowrap;
  transition: transform .35s var(--ease), background .3s, color .3s, border-color .3s, box-shadow .3s;
  position: relative; overflow: hidden;
  /* will-change is added by the magnetic handler on enter and dropped on
     leave — leaving it on permanently pins a compositor layer per button. */
}
.btn--sm { padding: 10px 16px; font-size: 13px; }
.btn--lg { padding: 18px 28px; font-size: 15px; }
.btn svg { transition: transform .35s var(--ease); }
.btn:hover svg { transform: translateX(4px); }

.btn--primary {
  background: var(--acc); color: #0a0a0c;
  box-shadow: 0 10px 30px -10px var(--acc-glow), inset 0 1px 0 rgba(255,255,255,0.3);
}
.btn--primary:hover {
  background: #84ff9c;
  box-shadow: 0 18px 40px -10px var(--acc-glow), inset 0 1px 0 rgba(255,255,255,0.4);
}

.btn--ghost {
  background: transparent; color: var(--ink);
  border: 1px solid var(--line-2);
}
.btn--ghost:hover {
  border-color: var(--ink-mute); background: rgba(255,255,255,0.04);
}

.btn--link {
  padding: 14px 0; color: var(--ink-soft); border-radius: 0;
}
.btn--link:hover { color: var(--acc); }

/* Magnetic hover (data-magnet applied via JS) */
.btn[data-magnet] { transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease), box-shadow .3s var(--ease); }

/* ---------- Navbar ---------- */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  padding: 18px var(--pad);
  /* Only transform/opacity animate. The old `is-stuck` state transitioned
     `padding`, which reflows the header subtree every frame; the same
     "compact bar" read is now an 8px compositor lift + a glass fade-in. */
  transition: transform .45s var(--ease);
}
.nav::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: rgba(10, 10, 12, 0.72);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  border-bottom: 1px solid var(--line);
  opacity: 0;
  transition: opacity .45s var(--ease);
}
.nav.is-stuck { transform: translateY(-8px); }
.nav.is-stuck::before { opacity: 1; }
/* Direction-aware: tuck away scrolling down, reveal scrolling up */
.nav.is-hidden { transform: translateY(-100%); }
.nav.is-hidden::before { opacity: 0; }
.nav__panel { display: contents; }
.nav__inner {
  max-width: var(--max); margin: 0 auto;
  display: grid; grid-template-columns: auto 1fr auto auto;
  align-items: center; gap: 24px;
}
.nav__brand {
  display: inline-flex; align-items: center; gap: 10px;
  color: var(--ink);
}
.nav__wordmark {
  font-size: 18px; font-weight: 600; letter-spacing: -0.02em;
}
.nav__wordmark em {
  font-family: var(--f-serif); color: var(--acc);
  font-size: 1.1em; line-height: 0.9;
}
.nav__wordmark--lg { font-size: 28px; }

.nav__links {
  justify-self: center;
  display: flex; gap: 28px;
  font-size: 14px; color: var(--ink-soft);
}
.nav__links a {
  position: relative; padding: 4px 0;
  transition: color .25s;
}
.nav__links a:hover { color: var(--ink); }
.nav__links a::after {
  content: ""; position: absolute; left: 0; bottom: -2px;
  width: 100%; height: 1px; background: var(--acc);
  /* scaleX instead of `width` — the underline grows on the compositor */
  transform: scaleX(0); transform-origin: left center;
  transition: transform .35s var(--ease);
}
.nav__links a:hover::after,
.nav__links a:focus-visible::after { transform: scaleX(1); }

.nav__cta {
  display: inline-flex; align-items: center; gap: 14px;
  font-size: 12px; color: var(--ink-mute);
  font-family: var(--f-mono);
}
.nav__pulse {
  width: 8px; height: 8px; border-radius: 50%; background: var(--acc);
  box-shadow: 0 0 0 0 var(--acc);
  animation: pulse 2.2s var(--ease) infinite;
}
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 var(--acc-glow); }
  70% { box-shadow: 0 0 0 14px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.nav__menu {
  display: none; width: 40px; height: 40px;
  position: relative; border: 1px solid var(--line-2); border-radius: 999px;
}
.nav__menu span {
  position: absolute; left: 10px; right: 10px; height: 1.5px;
  background: var(--ink); transition: transform .3s var(--ease);
}
.nav__menu span:nth-child(1) { top: 16px; }
.nav__menu span:nth-child(2) { bottom: 16px; }
.nav__menu.is-open span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.nav__menu.is-open span:nth-child(2) { transform: translateY(-8px) rotate(-45deg); }

/* ---------- Mobile nav drawer ----------
   Previously `.nav__links`/`.nav__cta` were simply `display:none` under 920px
   and the hamburger only morphed into an X — there was no panel to reveal, so
   navigation was unreachable on mobile. The drawer below fixes that. */
@media (max-width: 920px) {
  /* Only 2 grid items survive here (`.nav__panel` is display:none), so the
     desktop `auto 1fr auto auto` track list would drop the toggle into the
     `1fr` column — right next to the wordmark. `1fr auto` pins brand left,
     burger to the right edge, the conventional app placement. */
  .nav__inner { grid-template-columns: 1fr auto; }
  .nav__menu {
    display: inline-block; position: relative; z-index: 2;
    justify-self: end;
  }
  .nav__panel { display: none; }

  /* Grow the hit area to 48px without resizing the visible 40px circle: the
     hamburger spans are tuned to a 40px box (top/bottom 16px, 8px travel), so
     enlarging the button itself would break the morph into an X. */
  .nav__menu::after {
    content: ""; position: absolute; inset: -4px; border-radius: inherit;
  }

  /* A transformed ancestor becomes the containing block for `position: fixed`
     descendants — the open drawer would size to the 76px nav box instead of
     the viewport. Neutralise the stuck lift while the menu is open, and lift
     the header above the WhatsApp FAB (z-index 999). */
  .nav.is-open { transform: none; z-index: 1000; }

  .nav.is-open .nav__panel {
    display: grid; gap: 28px;
    position: fixed; inset: 0; z-index: 1;
    align-content: start;
    padding: 92px var(--pad) 40px;
    background: rgba(10, 10, 12, 0.97);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
    overflow-y: auto;
    overscroll-behavior: contain;
    opacity: 0;
    animation: navDrawerIn .32s var(--ease-out) forwards;
  }
  .nav.is-open .nav__links {
    display: grid; gap: 2px;
    /* Base rule sets `justify-self: center`, which shrink-wraps the grid to the
       widest label (~135px) and leaves the rest of the row dead to taps. */
    justify-self: stretch; width: 100%;
    font-size: 26px; font-weight: 500; letter-spacing: -0.03em;
    color: var(--ink);
  }
  .nav.is-open .nav__links a { padding: 14px 0; border-bottom: 1px solid var(--line); }
  .nav.is-open .nav__links a::after { display: none; }
  .nav.is-open .nav__cta {
    display: grid; gap: 14px; justify-items: start;
    font-size: 13px;
  }
  .nav.is-open .btn--sm { padding: 16px 24px; font-size: 15px; }
}
@keyframes navDrawerIn {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ---------- HERO ---------- */
.hero {
  position: relative; min-height: 100svh;
  padding: clamp(120px, 16vh, 180px) var(--pad) 80px;
  display: flex; align-items: center;
  overflow: hidden;
}
.hero__bg {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
}
.hero__grid {
  position: absolute; inset: -20%;
  background-image:
    linear-gradient(to right, var(--line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line) 1px, transparent 1px);
  background-size: 62px 62px;
  mask-image: radial-gradient(ellipse at 50% 40%, #000 30%, transparent 75%);
  -webkit-mask-image: radial-gradient(ellipse at 50% 40%, #000 30%, transparent 75%);
  opacity: .7;
  transform: perspective(1450px) rotateX(80deg) rotateZ(30deg) scale(1.6);
  transform-origin: 53% 44%;
}
.hero__orb {
  position: absolute; width: 480px; height: 480px;
  border-radius: 50%; filter: blur(80px); opacity: .35;
  /* 18s -> 16s: "sedikit lebih cepat", on top of the longer throw below. */
  animation: drift 16s ease-in-out infinite alternate;
}
.hero__orb--a {
  top: -120px; right: -80px;
  width: 720px; height: 720px;
  background: radial-gradient(circle, var(--acc-glow), transparent 60%);
  filter: blur(60px);
  opacity: .5;
  /* Sits top-right, so the shared `drift` (right + up) pushed it *away* from
     centre. Its own keyframe mirrors both axes so it sweeps down-left into the
     middle instead. Start point (no transform) is unchanged. */
  animation-name: driftA;
}
.hero__orb--b {
  bottom: -160px; left: -100px;
  background: radial-gradient(circle, #4d8aff 0%, transparent 70%);
  opacity: .22;
  animation-delay: -6s;
}
/* Throw lengthened 50px -> 75px (1.5x): orb B already travelled toward centre. */
@keyframes drift {
  to { transform: translate(60px, -45px); }
}
/* Orb A only: mirror of `drift`, i.e. toward the middle from the top-right. */
@keyframes driftA {
  to { transform: translate(-60px, 45px); }
}

.hero__inner {
  position: relative; z-index: 2;
  max-width: var(--max); margin: 0 auto; width: 100%;
  display: grid; gap: 56px;
}

.eyebrow {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 7px 14px 7px 12px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  font-family: var(--f-mono); font-size: 12px; color: var(--ink-soft);
  width: fit-content;
  background: rgba(255,255,255,0.02);
}
.eyebrow__dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--acc);
  box-shadow: 0 0 12px var(--acc);
}

.hero__title {
  font-size: clamp(38px, 8vw, 118px);
  line-height: 0.96;
  letter-spacing: -0.04em;
  font-weight: 500;
  color: var(--ink);
  /* The three lines are hard-broken in the markup; a 14ch cap forced the
     longest line ("Membangun teknologi") to wrap *inside* its mask box, which
     double-stacks the span and breaks the reveal. ~22ch keeps each authored
     line on one row down to the smallest breakpoint. */
  max-width: 22ch;
  text-wrap: balance;
}
/* Line-mask reveal. The `js` class (set by the inline head script) arms the
   masked state, so with scripting disabled the headline is simply visible.
   Descenders get 0.08em of extra room inside the clipping box, cancelled by a
   negative margin so the layout is unchanged. */
.hero__title .line {
  display: block; overflow: hidden;
  padding-bottom: .08em; margin-bottom: -.08em;
}
.hero__title .line > span { display: inline-block; }
.js .hero__title .line > span {
  transform: translateY(112%);
  opacity: 0;
  transition: transform 1.05s var(--ease-out), opacity .6s ease-out;
}
.js .hero__title .line:nth-child(1) > span { transition-delay: 160ms; }
.js .hero__title .line:nth-child(2) > span { transition-delay: 270ms; }
.js .hero__title .line:nth-child(3) > span { transition-delay: 380ms; }
.js .hero__title.is-ready .line > span { transform: translateY(0); opacity: 1; }
.hero__title em {
  font-family: var(--f-serif); font-style: italic; font-weight: 400;
  color: var(--acc);
}

.hero__sub {
  max-width: 56ch; display: grid; gap: 28px;
}
.hero__sub p {
  color: var(--ink-soft); font-size: 18px; line-height: 1.55;
  max-width: 60ch; text-wrap: pretty;
}
.hero__actions {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}

.hero__meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 28px 0;
  margin-top: 12px;
}
.hero__meta-item {
  display: grid; gap: 6px;
  padding: 0 24px;
  border-right: 1px solid var(--line);
}
.hero__meta-item:last-child { border-right: 0; }
.hero__meta-num {
  font-size: clamp(28px, 3.4vw, 40px);
  font-weight: 500; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.hero__meta-num em {
  font-family: var(--f-serif); font-style: italic; color: var(--acc);
  font-weight: 400; font-size: 1.05em;
}
.hero__meta-label {
  font-size: 12px; color: var(--ink-mute);
  font-family: var(--f-mono); text-transform: lowercase;
  letter-spacing: 0.02em;
}

@media (max-width: 720px) {
  .hero__meta { grid-template-columns: repeat(2, 1fr); }
  .hero__meta-item:nth-child(2) { border-right: 0; }
  .hero__meta-item:nth-child(1), .hero__meta-item:nth-child(2) { border-bottom: 1px solid var(--line); padding-bottom: 20px; }
  .hero__meta-item:nth-child(3), .hero__meta-item:nth-child(4) { padding-top: 20px; }
}

.hero__scroll {
  position: absolute; bottom: 32px; left: var(--pad);
  display: flex; align-items: center; gap: 12px;
  font-family: var(--f-mono); font-size: 11px; color: var(--ink-mute);
  letter-spacing: 0.1em; text-transform: uppercase;
  z-index: 2;
}
.hero__scroll-line {
  width: 60px; height: 1px; background: var(--line-2); position: relative; overflow: hidden;
}
.hero__scroll-line::after {
  content: ""; position: absolute; left: 0; top: 0; width: 50%; height: 100%;
  background: var(--acc);
  transform: translateX(-100%);
  animation: scrollLine 2.4s ease-in-out infinite;
}
@keyframes scrollLine {
  /* Used to animate `left`, which relayouts every frame. Same read, but the
     travel is now a compositor transform. */
  to { transform: translateX(200%); }
}

/* ---------- Marquee ---------- */
.marquee {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 28px 0;
  overflow: hidden;
  position: relative;
  background: var(--bg-soft);
}
.marquee::before, .marquee::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 120px; z-index: 2; pointer-events: none;
}
.marquee::before { left: 0; background: linear-gradient(to right, var(--bg-soft), transparent); }
.marquee::after  { right: 0; background: linear-gradient(to left, var(--bg-soft), transparent); }

.marquee__track {
  display: flex; gap: 48px; align-items: center;
  white-space: nowrap;
  font-family: var(--f-serif); font-style: italic; font-size: 32px;
  color: var(--ink-soft);
  animation: marquee 38s linear infinite;
  width: max-content;
}
.marquee__track > span[aria-hidden] { color: var(--acc); font-style: normal; font-size: 12px; }

@keyframes marquee {
  to { transform: translateX(-50%); }
}

/* ---------- Section head (shared) ---------- */
.section__head {
  max-width: var(--max); margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 24px;
  margin-bottom: 64px;
}
/* Scroll-linked progress rule (see the ScrollTrigger block in main.js).
   Driven purely by a custom property + scaleX, so it stays on the compositor. */
.section__head.has-progress { position: relative; }
.section__head.has-progress::after {
  content: ""; position: absolute; left: var(--pad); right: var(--pad); bottom: -28px;
  height: 1px; background: var(--acc); opacity: .7;
  transform: scaleX(var(--progress, 0)); transform-origin: left center;
}
.section__index {
  font-family: var(--f-mono); font-size: 12px;
  color: var(--ink-mute); letter-spacing: 0.1em; text-transform: uppercase;
}
.section__title {
  font-size: clamp(34px, 6vw, 84px);
  line-height: 1.02; letter-spacing: -0.035em;
  font-weight: 500; color: var(--ink);
  max-width: 18ch; text-wrap: balance;
}
.section__title em {
  font-family: var(--f-serif); font-style: italic;
  color: var(--acc); font-weight: 400;
}

/* ---------- SERVICES ---------- */
.services {
  padding: var(--section) 0;
  position: relative;
}
.services__grid {
  max-width: var(--max); margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 1px;
  grid-template-columns: repeat(3, 1fr);
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
@media (max-width: 920px) { .services__grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .services__grid { grid-template-columns: 1fr; } }

.card {
  position: relative;
  background: var(--bg);
  padding: 32px 28px 36px;
  display: grid; gap: 18px;
  min-height: 320px;
  transition: background .4s var(--ease);
  isolation: isolate;
  overflow: hidden;
}
.card__glow {
  position: absolute; inset: 0; z-index: -1; opacity: 0;
  background: radial-gradient(400px circle at var(--mx, 50%) var(--my, 50%),
    var(--acc-glow), transparent 50%);
  transition: opacity .4s var(--ease);
  pointer-events: none;
}
.card:hover { background: var(--bg-soft); }
.card:hover .card__glow { opacity: 1; }

.card__head {
  display: flex; align-items: center; justify-content: space-between;
  font-family: var(--f-mono); font-size: 11px;
  color: var(--ink-mute); letter-spacing: 0.08em; text-transform: uppercase;
}
.card__num { color: var(--acc); }

.card__title {
  font-size: 28px; line-height: 1.05; letter-spacing: -0.02em;
  font-weight: 500; color: var(--ink);
  text-wrap: balance;
}
.card__title em { color: var(--acc); }

.card__body { color: var(--ink-soft); font-size: 16px; line-height: 1.6; max-width: 38ch; }

.card__list {
  display: grid; gap: 8px;
  margin-top: auto;
  padding-top: 16px;
  border-top: 1px dashed var(--line-2);
}
.card__list li {
  font-family: var(--f-mono); font-size: 13.5px; color: var(--ink-soft);
  position: relative; padding-left: 22px;
}
.card__list li::before {
  content: "→"; position: absolute; left: 0; color: var(--acc);
}

.card--accent {
  background: linear-gradient(160deg, rgba(110,255,139,0.08), rgba(110,255,139,0.0) 60%);
  border: 1px solid var(--acc-glow);
}

/* ---------- PROCESS ---------- */
.process {
  padding: var(--section) 0;
  background: var(--bg-soft);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.process__list {
  max-width: var(--max); margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 0;
}
.process__step {
  display: grid;
  grid-template-columns: 80px 1fr;
  align-items: start;
  gap: 32px;
  padding: 36px 0;
  border-top: 1px solid var(--line);
  position: relative;
}
.process__step:last-child { border-bottom: 1px solid var(--line); }
.process__num, .process__body {
  /* The step itself can't take a hover transform — [data-reveal] owns
     `transform` on it. Shift the contents instead: compositor-only. */
  transition: transform .45s var(--ease);
}
.process__step::before {
  content: ""; position: absolute; left: 0; bottom: -1px;
  height: 1px; width: 100%; background: var(--acc);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .8s var(--ease-out);
}
.process__step:hover::before,
.process__step:focus-within::before { transform: scaleX(1); }
.process__step:hover .process__num,
.process__step:hover .process__body { transform: translateX(8px); }

.process__num {
  font-family: var(--f-mono); font-size: 13px; color: var(--acc);
  letter-spacing: 0.05em;
}
.process__step--active .process__num { color: var(--acc); }

.process__body h3 {
  font-size: 28px; font-weight: 500; letter-spacing: -0.02em;
  margin-bottom: 8px; color: var(--ink);
  text-wrap: balance;
}
.process__body p {
  color: var(--ink-soft); max-width: 56ch; font-size: 16.5px; line-height: 1.6;
}

@media (max-width: 720px) {
  .process__step { grid-template-columns: 60px 1fr; }
}

/* ---------- WORK ---------- */
.work {
  padding: var(--section) 0;
}
.work__list {
  max-width: var(--max); margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.work__item {
  display: grid;
  grid-template-columns: 140px 1fr auto auto;
  align-items: center;
  gap: 32px;
  padding: 32px 4px;
  background: var(--bg);
  position: relative;
  transition: background .35s var(--ease);
}
.work__item:hover { background: var(--bg-soft); }
/* [data-reveal] owns `transform` on the row, so the slide happens on its
   children — keeps the hover shift off the layout path entirely. */
.work__item > * { transition: transform .5s var(--ease), color .3s; }
.work__item:hover > * { transform: translateX(18px); }
.work__item::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: var(--acc);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .8s var(--ease-out);
}
.work__item:hover::after,
.work__item:focus-visible::after { transform: scaleX(1); }

.work__meta {
  display: flex; gap: 18px;
  font-family: var(--f-mono); font-size: 12px; color: var(--ink-mute);
  letter-spacing: 0.05em;
}
.work__index { color: var(--acc); }

.work__title {
  font-size: clamp(22px, 2.2vw, 30px);
  font-weight: 500; letter-spacing: -0.02em; color: var(--ink);
  line-height: 1.2; max-width: 32ch; text-wrap: balance;
}

.work__tags {
  display: flex; gap: 6px; flex-wrap: wrap;
  font-family: var(--f-mono); font-size: 12px; color: var(--ink-soft);
}
.work__tags span {
  padding: 4px 10px; border: 1px solid var(--line-2);
  border-radius: 999px; background: var(--bg-soft2);
}

.work__arrow {
  font-size: 28px; color: var(--ink-mute);
  transition: transform .4s var(--ease), color .3s;
}
/* Beats the generic `.work__item > *` slide so the arrow leads the row. */
.work__item:hover .work__arrow { transform: translateX(26px); color: var(--acc); }

@media (max-width: 860px) {
  .work__item { grid-template-columns: 1fr; gap: 12px; padding: 24px 4px; }
  .work__item:hover > * { transform: translateX(8px); }
  .work__arrow { display: none; }
}

/* ---------- STACK ---------- */
.stack {
  padding: var(--section) 0;
  background: var(--bg-soft);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.stack__grid {
  max-width: var(--max); margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 1px;
  grid-template-columns: repeat(6, 1fr);
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--rad);
  overflow: hidden;
}
@media (max-width: 920px) { .stack__grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 540px) { .stack__grid { grid-template-columns: repeat(2, 1fr); } }

.stack__col {
  background: var(--bg);
  padding: 28px 22px 24px;
  transition: background .3s;
}
.stack__col:hover { background: var(--bg-soft2); }
.stack__label {
  display: block; font-family: var(--f-mono); font-size: 11px;
  color: var(--acc); letter-spacing: 0.1em; text-transform: uppercase;
  margin-bottom: 16px;
}
.stack__col ul { display: grid; gap: 6px; }
.stack__col li {
  font-size: 15px; color: var(--ink); font-weight: 500;
  letter-spacing: -0.01em;
  padding: 4px 0;
  border-bottom: 1px dashed transparent;
  transition: color .25s, border-color .25s;
}
.stack__col li:hover { color: var(--acc); border-bottom-color: var(--acc-glow); }

/* ---------- QUOTES ---------- */
.quotes {
  padding: var(--section) 0;
}
.quotes__wrap {
  max-width: var(--max); margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 1px;
  grid-template-columns: repeat(3, 1fr);
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--rad);
  overflow: hidden;
}
@media (max-width: 860px) { .quotes__wrap { grid-template-columns: 1fr; } }

.quote {
  background: var(--bg);
  padding: 32px 28px 28px;
  display: grid; gap: 24px;
  margin: 0;
  transition: background .3s;
}
.quote:hover { background: var(--bg-soft); }
.quote blockquote {
  font-family: var(--f-serif); font-size: 22px; font-style: italic;
  line-height: 1.35; color: var(--ink);
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.quote figcaption {
  display: grid; gap: 2px;
  padding-top: 16px; border-top: 1px solid var(--line-2);
  font-size: 13px; color: var(--ink-mute);
}
.quote figcaption strong { color: var(--ink); font-weight: 500; }

/* ---------- CTA ---------- */
.cta {
  padding: var(--section) 0;
  background: var(--bg-soft);
  border-top: 1px solid var(--line);
  position: relative;
  overflow: hidden;
}
.cta::before {
  content: ""; position: absolute; top: -200px; left: 50%; transform: translateX(-50%);
  width: 800px; height: 800px; border-radius: 50%;
  background: radial-gradient(circle, var(--acc-glow), transparent 60%);
  filter: blur(60px); opacity: .5; pointer-events: none;
}
.cta__inner {
  max-width: 920px; margin: 0 auto;
  padding: 0 var(--pad);
  display: grid; gap: 48px;
  position: relative; z-index: 1;
}
.cta__title {
  font-size: clamp(40px, 6vw, 88px);
  line-height: 1.02; letter-spacing: -0.035em;
  font-weight: 500; color: var(--ink);
  text-wrap: balance;
}
.cta__title em { font-family: var(--f-serif); font-style: italic; color: var(--acc); font-weight: 400; }
.cta__sub {
  font-size: 18px; color: var(--ink-soft); line-height: 1.55;
  max-width: 56ch; text-wrap: pretty;
}
.cta__sub em { font-family: var(--f-serif); font-style: italic; color: var(--acc); }

/* WhatsApp call-to-action — this replaced the old contact form, and with it
   `.cta__form`, `.field*`, `.cta__submit` and `.cta__success` were deleted, as
   was the ~75-line validation block in main.js. The form only ever simulated a
   submit with a setTimeout; WhatsApp is the real channel, so there is nothing
   to fake and nothing to validate any more.

   Reuses `.btn--primary`, which brings the site's own accent green rather than
   WhatsApp's #25D366 — the palette has exactly one accent — and inherits the
   magnetic hover the other primary buttons get from main.js.

   `.btn:hover svg` slides an icon right, which is an arrow affordance; a brand
   mark must not do that. Declared after the Buttons block so it wins on order. */
.cta__wa {
  justify-self: start;   /* .cta__inner is a grid — stop the pill stretching full width */
}
.cta__wa:hover svg { transform: none; }
.cta__wa-icon { width: 22px; height: 22px; flex: none; }

/* ---------- FOOTER ---------- */
.foot {
  padding: 80px var(--pad) 28px;
  background: var(--bg);
  border-top: 1px solid var(--line);
}
.foot__top {
  max-width: var(--max); margin: 0 auto;
  display: grid; grid-template-columns: 1.2fr 2fr; gap: 60px;
  padding-bottom: 60px;
  border-bottom: 1px solid var(--line);
}
@media (max-width: 800px) { .foot__top { grid-template-columns: 1fr; gap: 40px; } }

.foot__brand p {
  color: var(--ink-mute); font-size: 14px; margin-top: 16px;
  max-width: 36ch;
}
.foot__cols {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px;
}
@media (max-width: 700px) { .foot__cols { grid-template-columns: repeat(2, 1fr); gap: 28px; } }

.foot__cols > div { display: grid; gap: 10px; }
.foot__label {
  font-family: var(--f-mono); font-size: 11px; color: var(--acc);
  letter-spacing: 0.1em; text-transform: uppercase;
  margin-bottom: 6px;
}
.foot__cols a {
  font-size: 14px; color: var(--ink-soft);
  transition: color .25s, transform .25s;
  display: inline-block;
}
.foot__cols a:hover { color: var(--acc); transform: translateX(2px); }

.foot__bot {
  max-width: var(--max); margin: 24px auto 0;
  display: flex; justify-content: space-between; align-items: center;
  font-family: var(--f-mono); font-size: 12px; color: var(--ink-mute);
  flex-wrap: wrap; gap: 12px;
}
.foot__sig { display: inline-flex; align-items: center; gap: 8px; }
.foot__sig-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--acc);
  box-shadow: 0 0 8px var(--acc);
  animation: pulse 2.2s ease-in-out infinite;
}

/* ---------- WhatsApp Sticky Button ---------- */
.whatsapp-float {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  z-index: 999;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  box-shadow: 0 4px 20px rgba(37, 211, 102, 0.4);
  /* `all` also animated width/height/border-radius on every hover; name the
     two properties that actually change. */
  transition: transform .3s var(--ease), box-shadow .3s var(--ease);
  animation: whatsappPulse 2s ease-in-out infinite;
}

.whatsapp-float img {
  /* 105% pushed the icon past the 64px circle; clip it instead. */
  width: 104%;
  height: 104%;
  object-fit: cover;
}

.whatsapp-float:hover {
  transform: scale(1.1);
  box-shadow: 0 6px 28px rgba(37, 211, 102, 0.5);
}

.whatsapp-float:active {
  transform: scale(0.95);
}

@keyframes whatsappPulse {
  0%, 100% {
    box-shadow: 0 4px 20px rgba(37, 211, 102, 0.4);
  }
  50% {
    box-shadow: 0 4px 28px rgba(37, 211, 102, 0.6), 0 0 0 8px rgba(37, 211, 102, 0.15);
  }
}

@media (max-width: 640px) {
  .whatsapp-float {
    /* No `bottom` here: the dock section at the end of this file owns the
       vertical position at every width where the bar is visible. */
    right: 1rem;
    width: 64px;
    height: 64px;
  }
}

/* ---------- Hero Grid Control Panel (dev) ---------- */
.grid-panel__toggle {
  position: fixed; left: 1rem; bottom: 1rem; z-index: 998;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--bg-soft); border: 1px solid var(--line-2);
  color: var(--ink); display: grid; place-items: center;
  font-size: 18px; cursor: pointer;
  transition: transform .3s var(--ease), background .3s;
}
.grid-panel__toggle:hover { background: var(--bg); transform: rotate(45deg); }
/* The rules above set an explicit `display`, which beats the UA stylesheet's
   `[hidden] { display: none }` — so the dev tools stay hidden until ?dev=1. */
.grid-panel__toggle[hidden] { display: none; }
.grid-panel[hidden] { display: none; }

.grid-panel {
  position: fixed; top: 0; right: 0; height: 100dvh; width: 300px;
  max-width: 86vw; z-index: 997;
  background: rgba(10, 10, 12, .92);
  backdrop-filter: blur(16px);
  border-left: 1px solid var(--line-2);
  transform: translateX(100%); transition: transform .35s var(--ease);
  display: flex; flex-direction: column;
}
.grid-panel.is-open { transform: translateX(0); }

.grid-panel__head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 18px; border-bottom: 1px solid var(--line-2);
}
.grid-panel__title {
  font-family: var(--f-mono); font-size: 12px;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft);
}
.grid-panel__close {
  width: 28px; height: 28px; display: grid; place-items: center;
  border-radius: 50%; font-size: 18px; line-height: 1;
  color: var(--ink-soft); cursor: pointer;
}
.grid-panel__close:hover { color: var(--ink); background: var(--bg-soft); }

.grid-panel__body {
  overflow-y: auto; padding: 12px 18px 24px; display: grid; gap: 14px;
}
.grid-panel__field { display: grid; gap: 6px; }
.grid-panel__label {
  font-size: 12px; font-family: var(--f-mono); color: var(--ink-soft);
  display: flex; justify-content: space-between; align-items: center;
}
.grid-panel__val { color: var(--acc); }
.grid-panel__field input[type="range"] { accent-color: var(--acc); width: 100%; cursor: pointer; }
.grid-panel__reset {
  margin-top: 6px; padding: 10px; border: 1px solid var(--line-2);
  border-radius: 8px; color: var(--ink-soft); cursor: pointer;
  font-family: var(--f-mono); font-size: 12px;
  transition: border-color .25s, color .25s;
}
.grid-panel__reset:hover { border-color: var(--acc); color: var(--acc); }
.grid-panel__copy {
  margin-top: 2px; padding: 10px; border: 1px solid var(--acc);
  border-radius: 8px; background: var(--acc); color: #0a0a0c; cursor: pointer;
  font-family: var(--f-mono); font-size: 12px;
  transition: filter .25s;
}
.grid-panel__copy:hover { filter: brightness(1.1); }

/* ---------- Scrollbar ----------
   Ported from PROPOSAL-POS-APOTEK, retuned to this theme's single accent.
   `color-scheme: dark` on :root already gives both engines a dark default;
   these rules only replace the WebKit paint so the thumb reads as signal
   green. Keep `width` in sync with the thumb border — without it the UA
   default (~15px) swallows the 2px inset and the radius looks squashed. */
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--bg-soft2); }
::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, var(--acc), #2f9e52);
  border-radius: 10px;
  border: 2px solid var(--bg-soft2);
}
::-webkit-scrollbar-thumb:hover { background: var(--acc); }

/* Firefox fallback — MUST stay inside this feature query.
   `scrollbar-color`/`scrollbar-width` at anything other than `auto` make the
   engine ignore ::-webkit-scrollbar-* entirely (per spec, and because the
   property is inherited it also leaks into every scroll container). Setting
   them unconditionally killed the gradient above in Chrome. Chrome parses
   `::-webkit-scrollbar` so this block is skipped there; Firefox fails the
   selector test and gets the flat-colour fallback it needs. */
@supports not selector(::-webkit-scrollbar) {
  html {
    scrollbar-width: thin;
    scrollbar-color: var(--acc) var(--bg-soft2);
  }
}

/* ---------- ScrollSmoother ----------
   The plugin writes the wrapper/content CSS itself at runtime (it snapshots and
   restores their inline cssText on kill), so we deliberately do NOT hard-code
   position/overflow/height here. We only make sure the content block spans the
   full width; horizontal clipping is handled by `overflow-x: clip` on <body>. */
#smooth-content { width: 100%; }

/* ---------- Mobile dock tab bar ----------
   Five section shortcuts for one-handed use. Deliberately *additive*: the burger
   and the drawer are untouched. The drawer paints over the dock while open
   (`.nav.is-open` is z-index 1000, the dock 998), so nothing needs to hide it.

   Mark-up lives next to `.whatsapp-float`, outside #smooth-wrapper — a
   transformed ancestor becomes the containing block for `position: fixed`
   descendants, and ScrollSmoother transforms #smooth-content.

   No `backdrop-filter`, even though the drawer uses one: the drawer is
   momentary, this bar sits above a scrolling page indefinitely and would
   re-blur on every frame. At 0.97 alpha the blur is imperceptible anyway.

   Works with no JS: these are plain <a href="#..."> links, and main.js's anchor
   handler is delegated by selector, so the bar needs no script to navigate.
   Only the active-tab highlight below is progressive enhancement. */
.dock { display: none; }

@media (max-width: 920px) {
  .dock {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    position: fixed; left: 0; right: 0; bottom: 0;
    z-index: 998;
    height: var(--dock-safe);
    padding-bottom: env(safe-area-inset-bottom);
    background: rgba(10, 10, 12, 0.97);
    border-top: 1px solid var(--line);
  }

  .dock__item {
    position: relative;
    display: grid; justify-items: center; align-content: center; gap: 4px;
    min-height: 44px;            /* WCAG 2.5.5 / HMT touch target */
    padding: 8px 2px;
    color: var(--ink-mute);
    /* colour + transform only; `transition: all` would animate layout props */
    transition: color .25s, transform .25s var(--ease);
  }
  /* Guarded: on touch this bar is the only hover target that never leaves the
     screen, and a stuck :hover would make a tab look half-active after a tap. */
  @media (hover: hover) {
    .dock__item:hover { color: var(--ink-soft); }
  }
  .dock__item:active { transform: scale(.94); }

  /* Active tick. Grows with scaleX on the compositor — never `width`, which the
     audit pass just stripped from this file. */
  .dock__item::before {
    content: ""; position: absolute; top: -1px; left: 50%;
    width: 24px; height: 2px;
    background: var(--acc);
    transform: translateX(-50%) scaleX(0);
    transition: transform .3s var(--ease);
  }
  .dock__item[aria-current="true"] { color: var(--acc); }
  .dock__item[aria-current="true"]::before { transform: translateX(-50%) scaleX(1); }

  .dock__icon { width: 21px; height: 21px; }
  .dock__label {
    font-family: var(--f-mono); font-size: 10px; line-height: 1.1;
    letter-spacing: 0.01em;
    white-space: nowrap;
  }

  /* Lift the chrome that used to own the bottom band. Source order puts these
     above the original declarations. */
  .foot           { padding-bottom: calc(28px + var(--dock-safe)); }
  .whatsapp-float { bottom: calc(1.5rem + var(--dock-safe)); }
  .grid-panel__toggle { bottom: calc(1rem + var(--dock-safe)); }

  /* The "Scroll" hint is decorative (aria-hidden) and its whole job — "there is
     more below, and here is how you move" — is now done by a permanent bar.
     Lifting it instead would crowd the 2x2 stats grid on short viewports. */
  .hero__scroll { display: none; }
}

@media (max-width: 640px) {
  .whatsapp-float { bottom: calc(1rem + var(--dock-safe)); }
}

/* No label size step-down. Measured in Chromium at a forced 10px: the widest
   label ("Layanan") is 42.7px and the narrowest column is 64px at a 320px
   viewport, so five columns never collide. 10px is kept all the way down,
   rather than 9.5px on every phone from 320-400px wide for a problem that
   does not exist. Shortening "Studi Kasus" to "Kasus" is what buys this. */


/* ---------- Reduced motion ----------
   Covers the dock too: the global rule above zeroes the tick's scaleX
   transition, so the active tab snaps instead of animating. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
    /* Without this, `infinite` animations (marquee, pulse, drift) keep looping
       as fast as the compositor allows instead of stopping. */
    animation-iteration-count: 1 !important;
  }
  /* The `.js` prefix on the arming rules out-specifies these, so repeat it. */
  .js [data-reveal], [data-reveal] { opacity: 1; transform: none; }
  .js .hero__title .line > span { opacity: 1; transform: none; }
  /* ScrollSmoother is not created under reduced motion, so neutralise the
     global `html { scroll-behavior: smooth }` for these users too. */
  html { scroll-behavior: auto; }
}
