/* ─── RESET & BASE ──────────────────────────────── */
html { scroll-behavior: smooth; }

/* ─── TSPARTICLES CONTAINER ─────────────────────── */
#tsparticles {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}


body {
  /* ── Base: flat page color ── */
  background-color: var(--bg-page, #FFFFFF) !important;
  background-image: none !important;
  transition: background-color .4s var(--ease-soft);
 -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Global keyboard focus visibility ──
   Previously only .cf-field inputs had a focus ring, which meant nav
   links, filter tabs, carousel controls, and social icons were
   invisible to keyboard users. This restores a visible, on-brand
   focus indicator everywhere, without adding a ring on mouse click
   (:focus-visible only fires for keyboard/programmatic focus). */
/* ── Hero social icons ──
   Replaces per-link inline styles + onmouseenter/onmouseleave JS
   (which had no keyboard/touch equivalent and hardcoded hex colors
   instead of the theme variables). Brand color on hover is set via a
   data attribute per icon; base state matches the site's neutral
   surface/border tokens. */
.hero-social-btn {
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 10px;
  border: 1px solid var(--glass-border, #e2e8f0);
  background: var(--surface, #fff);
  color: var(--text-primary, #0f172a);
  text-decoration: none;
  transition: background .2s ease, border-color .2s ease, color .2s ease, transform .2s ease;
}
.hero-social-btn:hover,
.hero-social-btn:focus-visible {
  background: var(--brand, var(--primary));
  border-color: var(--brand, var(--primary));
  color: #fff;
  transform: translateY(-2px);
}
.hero-social-btn[data-brand="linkedin"]   { --brand: #0077b5; }
.hero-social-btn[data-brand="github"]     { --brand: #24292e; }
.hero-social-btn[data-brand="instagram"]  { --brand: #e1306c; }
.hero-social-btn[data-brand="email"]      { --brand: #3b82f6; }

a:focus-visible,
button:focus-visible,
[role="tab"]:focus-visible,
[role="button"]:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--primary, #2563eb);
  outline-offset: 3px;
  border-radius: 4px;
}

:root {
  /* ══════════════════════════════════════════════════════════
     DESIGN TOKENS — "modern digital portfolio" palette
     ══════════════════════════════════════════════════════════ */
  --blue:      #2563eb;
  --blue-lt:   rgba(37, 99, 235, 0.15);
  --dark:      #0f172a;
  --gray:      #334155;
  --border:    transparent;
  --surface:   #e8e8e8;
  --bg-color:  #e8e8e8;
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);

  /* Neumorphic dual-tone soft shadows (light mode only construct) */
  --neu-bg:      #e8e8e8;
  --neu-light:   #ffffff;
  --neu-dark:    #c3c3c3;
  --neu-shadow-sm: 4px 4px 10px var(--neu-dark), -4px -4px 10px var(--neu-light);
  --neu-shadow:    8px 8px 18px var(--neu-dark), -8px -8px 18px var(--neu-light);
  --neu-shadow-lg: 16px 16px 32px var(--neu-dark), -16px -16px 32px var(--neu-light);
  --neu-inset-sm:  inset 2px 2px 5px var(--neu-dark), inset -2px -2px 5px var(--neu-light);
  --neu-inset:     inset 4px 4px 10px var(--neu-dark), inset -4px -4px 10px var(--neu-light);

  /* ── Semantic tokens (new) ── */
  --bg-page:        #f6f7fb;
  --surface-1:       rgba(255, 255, 255, 0.65);   /* nav / elevated glass */
  --surface-2:       rgba(255, 255, 255, 0.40);   /* filter chips, subtle glass */
  --card-bg:         var(--surface);
  --glass-border:    var(--glass-border);
  --glass-border-hi: rgba(255, 255, 255, 0.85);
  --text-primary:    #0f172a;
  --text-secondary:  #475569;
  --text-tertiary:   #94a3b8;
  --primary:         #2563eb;
  --primary-hover:   #1d4ed8;
  --primary-tint:    rgba(37, 99, 235, 0.15);
  --shadow-color:    15, 23, 42;
  --shadow-sm: 0 4px 16px rgba(var(--shadow-color), 0.05);
  --shadow-md: 0 12px 32px rgba(var(--shadow-color), 0.08);
  --shadow-lg: 0 20px 48px rgba(var(--shadow-color), 0.10);
  --ambient-glow:    rgba(37, 99, 235, 0.14);
  --network-line:    rgba(15, 23, 42, 0.10);
  --network-node:    rgba(15, 23, 42, 0.16);
  --spotlight-color: rgba(59, 130, 246, 0.10);
  --terminal-bg:     var(--dark);
  --terminal-bar:    #1e293b;
  --terminal-border: #334155;
  --spotlight-blend: multiply;
}



html {
  height: 100%;
  overflow: hidden;
  overflow-x: hidden !important;
}

body {
  font-family: 'Inter', sans-serif;
  color: var(--text-primary);
  height: 100%;
  overflow: hidden;
  overflow-x: hidden !important;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}

/* teks di seluruh web sengaja gak bisa di-select — supaya navigasi
   swipe pakai klik-tarik mouse gak pernah kebentrok/nyangkut sama
   seleksi teks native browser (dua-duanya rebutan gesture yang sama) */
* {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
input, textarea, select {
  -webkit-user-select: text;
  -moz-user-select: text;
  -ms-user-select: text;
  user-select: text;
}



/* ─── SHARED: NAVBAR ───────────────────────────── */
nav {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 32px; height: 72px;
  border: 1px solid var(--border);
  border-radius: 99px;
  position: fixed;
  top: 24px;
  /* Centering via left+right+auto margin — lebih reliable dari left:50%+transform */
  left: 0;
  right: 0;
  margin: 0 auto;
  z-index: 100;
  background: var(--surface-1);
  backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
  width: 90%; max-width: 1200px;
  box-shadow: 0 12px 32px rgba(37, 99, 235, 0.08), 0 2px 10px rgba(0, 0, 0, 0.02);
  transition: transform 0.4s var(--ease-soft), box-shadow 0.4s var(--ease-soft);
}
nav:hover {
  box-shadow: 0 16px 48px rgba(37, 99, 235, 0.12), 0 4px 16px rgba(0, 0, 0, 0.04);
  transform: translateY(-2px);
}
.nav-logo { font-size: 15px; font-weight: 700; letter-spacing: -0.02em; }
.nav-logo .accent { color: var(--blue); }
.nav-links { display: flex; gap: 8px; list-style: none; }
.nav-links a {
  font-size: 13px; font-weight: 500;
  padding: 8px 16px; border-radius: 99px;
  color: var(--text-secondary); text-decoration: none;
  transition: background 0.3s var(--ease-soft), color 0.3s var(--ease-soft);
}
.nav-links a:hover  { background: rgba(0,0,0,0.05); color: var(--dark); }
.nav-links a.active { background: var(--dark); color: #fff; }
.nav-status {
  display: flex; align-items: center; gap: 8px;
  font-size: 12px; font-weight: 600; color: #059669;
  background: #ecfdf5; padding: 6px 14px;
  border-radius: 99px; border: 1px solid #a7f3d0;
}
.nav-status .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: #059669; animation: pulse 2s infinite;
}
@keyframes pulse {
  0%,100% { opacity:1; transform:scale(1); }
  50%     { opacity:.4; transform:scale(1.5); }
}

@media (max-width: 768px) {
  nav { padding: 0 20px; width: 95%; max-width: none; top: 16px; border-radius: 50px; }
  .nav-links { display: none; }
}

/* ─── SHARED: HEADER & TYPOGRAPHY ───────────────── */
.page-header {
  padding: 56px 5vw 32px; max-width: 1280px; margin: 0 auto; width: 100%;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 32px;
  opacity: 0; animation: fadeUp .6s .1s var(--ease-soft) forwards;
}
.page-eyebrow { 
  font-size: 11px; font-weight: 700; letter-spacing: 0.1em; 
  color: var(--blue); margin-bottom: 12px; text-transform: uppercase;
}
.page-title { 
  font-size: clamp(32px, 4vw, 56px); font-weight: 900; 
  letter-spacing: -0.04em; line-height: 1.1; color: var(--text-primary);
}
.page-sub { 
  font-size: 15px; color: var(--text-secondary); line-height: 1.7; 
  margin-top: 16px; max-width: 480px; font-weight: 400;
}
.divider { border: none; border-top: 1px solid var(--border); margin: 0 5vw; }

/* ── WORKS HERO — legacy spacer, now unused on desktop ──
   Originally reserved the ~108px nav-clearance gap at the top of
   #works. Now that the wheel-jack sheets (.works-projects/
   .works-activities) are full-bleed and supply their own internal
   nav-clearance padding (see #works and .works-projects in the
   desktop layout section), this div sitting before .works-views in
   the flex column would just leave a stray ~18px sliver of #works's
   own background peeking out above the sheets — hidden entirely on
   desktop. Mobile still uses it as breathing room before the plain
   stacked Projects/Activities flow, see the max-width:640px override. */
#works .works-hero {
  flex-shrink: 0;
  max-width: 1280px;
  margin: 0 auto;
  width: 100%;
  padding: 0 5vw 18px;
  box-sizing: border-box;
  opacity: 0;
}
@media (min-width: 641px) {
  #works .works-hero { display: none; }
}
#works.page-active .works-hero {
  animation: slideUpIn .7s cubic-bezier(0.17, 0.55, 0.55, 1) forwards;
  animation-delay: .05s;
}
.works-hero-title {
  /* Sama font-family/weight dengan .hero-heading (Home) — itu sudah
     didapat lewat rule !important di bagian TYPOGRAPHY REDESIGN (Plus
     Jakarta Sans 700). Ukurannya sengaja tidak dibuat sebesar heading
     Home (clamp 40–68px): "Selected Activities" lebih panjang dari
     "Selected Works", dan floor 40px membuatnya berpotensi overflow/
     wrap aneh di layar sempit karena clamp() tidak bisa turun di bawah
     floor-nya. clamp(28px, 4.4vw, 56px) tetap besar & bold seperti
     Home, tapi punya ruang menyusut lebih jauh di mobile. */
  font-size: clamp(28px, 4.4vw, 56px);
  font-weight: 900;
  letter-spacing: -0.02em;
  line-height: 1.08;
  color: var(--text-primary);
  margin: 0;
}
/* Aksen warna biru pada kata terakhir judul (mis. "WORKS" / "ACTIVITIES"),
   meniru .hl pada .hero-heading di Home. .works-hero-title selalu berupa
   2 kata ("SELECTED WORKS" / "SELECTED ACTIVITIES"), dan BlurText memecah
   teks jadi 1 <span class="blur-word"> per kata secara berurutan — jadi
   nth-child(2) otomatis menarget kata kedua tanpa perlu ubah JS/markup. */
.works-hero-title .blur-word:nth-child(2) {
  background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  font-weight: 800;
}
.works-hero-sub {
  font-size: 15px;
  color: var(--text-secondary);
  line-height: 1.55;
  margin: 8px 0 0;
  max-width: 620px;
  font-weight: 400;
}

/* ── ACTIVITIES HERO — same concept as .works-hero above (identical
   box, identical .works-hero-title/-sub typography). Used mid-scroll
   for the "Selected Activities" chapter instead of introducing a
   second, smaller heading style. Renders statically now — the
   scroll-in blur-text reveal + replay/reset behavior was removed on
   request, so there's no opacity:0 base state or .is-in animation to
   wait on anymore. */
.works-activities-hero {
  max-width: 1280px;
  margin: 0 auto;
  width: 100%;
  /* Top padding now doubles as the Activities sheet's own nav-clearance
     spacer — #works no longer reserves that 108px itself (see the
     #works rule in the desktop layout section), so this empty div,
     already sitting first inside the full-bleed sheet, is what keeps
     the masonry grid below it from starting under the floating nav. */
  padding: 108px 5vw 18px;
  box-sizing: border-box;
}

/* ── BlurText — vanilla-JS port of React Bits <BlurText /> ──
   The JS helper (see the inline script by works-fan-carousel.js) wraps
   each word in a .blur-word span and animates filter/opacity/transform
   directly via the Web Animations API (el.animate), reproducing the
   same keyframes/timing as the source React component. This CSS just
   sets the base layout + the initial hidden state so words don't flash
   at full opacity before the script runs. */
.works-hero-title,
.works-hero-sub {
  display: flex;
  flex-wrap: wrap;
}
.blur-word {
  display: inline-block;
  opacity: 0;
  filter: blur(10px);
  will-change: transform, filter, opacity;
}
@media (prefers-reduced-motion: reduce) {
  .blur-word { opacity: 1 !important; filter: none !important; transform: none !important; }
}

/* ─── SHARED: FILTERS & TABS ────────────────────── */
.filters, .filter-bar {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
  opacity: 0; animation: fadeUp .6s .2s var(--ease-soft) forwards;
}
.filter-bar { padding: 24px 5vw 0; max-width: 1280px; margin: 0 auto; width: 100%; }
.filter-label { font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--text-secondary); margin-right: 8px; text-transform: uppercase; }
.filter-btn {
  position: relative;
  font-size: 12px; font-weight: 600; padding: 8px 18px; border-radius: 99px;
  border: 1px solid var(--glass-border); background: var(--surface-2); color: var(--gray);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  cursor: pointer; transition: all 0.3s var(--ease-soft);
}
.filter-btn:hover  { border-color: var(--glass-border-hi); color: var(--dark); background: var(--surface-1); }
.filter-btn.active { background: var(--dark); color: #fff; border-color: var(--dark); }
/* Custom filter colors */
.filter-btn.active[data-cat="iot"], .filter-btn.active[data-filter="iot"]        { background:#2563eb; border-color:#2563eb; }
.filter-btn.active[data-cat="ml"], .filter-btn.active[data-filter="ml"]         { background:#7C3AED; border-color:#7C3AED; }
.filter-btn.active[data-cat="cv"], .filter-btn.active[data-filter="cv"]         { background:#059669; border-color:#059669; }
.filter-btn.active[data-cat="web"], .filter-btn.active[data-filter="web"]        { background:#D97706; border-color:#D97706; }
.filter-btn.active[data-cat="design"], .filter-btn.active[data-filter="design"]     { background:#DB2777; border-color:#DB2777; }
.filter-btn.active[data-cat="cloud"]      { background:#15803D; border-color:#15803D; }
.filter-btn.active[data-cat="course"]     { background:#0891B2; border-color:#0891B2; }
.filter-btn.active[data-cat="training"]   { background:#D97706; border-color:#D97706; }
.filter-btn.active[data-cat="compete"]    { background:#E11D48; border-color:#E11D48; }
.filter-btn.active[data-cat="internship"] { background:#9333EA; border-color:#9333EA; }

/* ─── SHARED: TECH STACK ROW ────────────────────── */
.stack-row {
  border-top: 1px solid var(--border); padding: 20px 5vw; display: flex; align-items: center; gap: 32px; background: var(--surface); margin-top: auto;
}
.stack-label, .stack-row-label { 
  font-size: 10px; font-weight: 700; color: var(--text-secondary); letter-spacing: .12em; flex-shrink: 0; text-transform: uppercase;
}
.stack-items, .stack-row-items { display: flex; gap: 24px; flex-wrap: wrap; }
.stack-item, .stack-row-item  { font-size: 12px; font-weight: 600; color: var(--text-secondary); transition: color 0.3s var(--ease-soft); cursor: default; }
.stack-item:hover, .stack-row-item:hover { color: var(--blue); }

footer { padding: 24px 5vw; border-top: 1px solid var(--border); display: flex; justify-content: center; background: var(--surface); }
footer p { font-size: 13px; color: var(--text-secondary); }

/* ─── INDEX: HERO ─────────────────────────────────── */
.hero {
  flex: 1; display: flex; align-items: center;
  padding: 32px 5vw 32px; gap: 48px;
  max-width: 1280px; margin: 0 auto; width: 100%;
  min-height: calc(100vh - 108px);
}
.hero-left { flex: 1; min-width: 0; }
.hero-right {
  flex: 0 0 380px;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; animation: fadeIn .9s .25s var(--ease-soft) forwards;
}

.skill-pills {
  display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 32px; min-height: 34px;
  opacity: 0; animation: fadeUp .6s .1s var(--ease-soft) forwards;
}
.pill-typing {
  display: inline-flex; align-items: center; font-size: 11px; font-weight: 700;
  padding: 6px 14px; border-radius: 99px; border: 1px solid #bfdbfe; letter-spacing: .02em;
  background: var(--blue-lt); color: var(--blue);
  transition: all 0.4s var(--ease-soft);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
}
.pill-cursor { margin-left: 2px; font-weight: 300; animation: pillBlink .65s infinite; }
@keyframes pillBlink { 0%,100%{opacity:1} 50%{opacity:0} }

.hero-heading {
  font-size: clamp(40px, 5vw, 68px); font-weight: 900; line-height: 1.05; letter-spacing: -0.04em; margin-bottom: 24px;
  opacity: 0; animation: fadeUp .65s .2s var(--ease-soft) forwards; color: var(--text-primary);
}
.hero-heading .hl { color: var(--blue); }
.hero-sub {
  font-size: 16px; color: var(--text-secondary); line-height: 1.7; max-width: 500px; margin-bottom: 40px;
  opacity: 0; animation: fadeUp .65s .3s var(--ease-soft) forwards;
}

.terminal-box {
  position: relative;
  background: var(--terminal-bg); border-radius: 12px; overflow: hidden; max-width: 480px;
  opacity: 0; animation: fadeUp .65s .42s var(--ease-soft) forwards;
  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.1); border: 1px solid var(--terminal-border);
  transition: background .4s var(--ease-soft), border-color .4s var(--ease-soft), box-shadow .4s var(--ease-soft);
}
/* very subtle scanline — identity element, not a gimmick */
.terminal-box::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: repeating-linear-gradient(180deg, rgba(255,255,255,0.025) 0px, rgba(255,255,255,0.025) 1px, transparent 1px, transparent 3px);
  opacity: 0; transition: opacity .4s var(--ease-soft);
}
.terminal-bar {
  display: flex; align-items: center; gap: 8px; padding: 12px 18px;
  background: var(--terminal-bar); border-bottom: 1px solid var(--terminal-border);
  transition: background .4s var(--ease-soft), border-color .4s var(--ease-soft);
}
.terminal-bar span { width: 12px; height: 12px; border-radius: 50%; }
.t-r{background:#ef4444;} .t-y{background:#eab308;} .t-g{background:#22c55e;}
.terminal-title { font-family:'JetBrains Mono',monospace; font-size:12px; color:#8592a8; margin-left:8px; }
.terminal-body  { padding:20px 24px; font-family:'JetBrains Mono',monospace; font-size:14px; min-height:80px; position: relative; z-index: 2; }
.t-prompt{color:#34d399;} .t-text{color:#f8fafc;}
.t-cursor { display:inline-block; width:2px; height:16px; background:#f8fafc; vertical-align:middle; margin-left:2px; animation: blink .7s infinite; }

.hero-socials {
  display: flex; gap: 14px; margin-top: 48px;
  opacity: 0; animation: fadeUp .65s .54s var(--ease-soft) forwards;
}
.hero-socials a {
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--glass-border); border-radius: 99px; background: var(--surface);
  color: var(--text-secondary); text-decoration: none;
  transition: all 0.3s var(--ease-soft);
}
.hero-socials a:hover { border-color:var(--blue); color:var(--blue); transform:translateY(-3px); box-shadow: 0 4px 12px rgba(37, 99, 235,0.1); }


/* ─── HERO RIGHT: PHOTO CARD (gaya gambar referensi) ── */
.hero-photo-wrap {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* soft ambient aura breathing behind the card — adds depth without being loud */
.hero-photo-wrap::before {
  content: '';
  position: absolute;
  width: 300px; height: 300px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(37, 99, 235,0.16) 0%, rgba(139,92,246,0.08) 45%, transparent 75%);
  filter: blur(18px);
  z-index: 0;
  animation: auraPulse 6s ease-in-out infinite;
}

@keyframes auraPulse {
  0%, 100% { transform: scale(0.94); opacity: 0.7; }
  50%      { transform: scale(1.06); opacity: 1; }
}

.hero-photo-card {
  position: relative;
  width: 270px;
  height: 360px;
  z-index: 1;
  /* Neumorphic soft card */
  background: #e0e0e0;
  border-radius: 50px;
  padding: 16px;
  box-shadow: 20px 20px 60px #bebebe, -20px -20px 60px #ffffff;
  transition: transform 0.45s var(--ease-soft), box-shadow 0.45s var(--ease-soft), background .4s var(--ease-soft);
  /* gentle ambient float — idle motion so the photo feels alive even without hover */
  animation: photoFloat 5.5s ease-in-out infinite;
}
.hero-photo-card:hover {
  animation-play-state: paused;
  transform: translateY(-6px) rotate(-0.8deg);
}

@keyframes photoFloat {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-7px) rotate(0.4deg); }
}

.hero-card-img {
  width: 100%;
  aspect-ratio: 3/4;
  object-fit: cover;
  object-position: center 8%;
  display: block;
  border-radius: 36px;  /* rounded corner dalam card */
  filter: saturate(1.05);
  transition: transform 0.5s ease;
}
.hero-photo-card:hover .hero-card-img {
  transform: scale(1.02);
}

/* Badge floating di sekitar card */
.fbadge {
  position: absolute; z-index: 3; background: var(--surface-1);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(200, 220, 255, 0.5); border-radius: 10px;
  padding: 7px 14px; font-size: 11px; font-weight: 700; color: var(--text-primary);
  box-shadow: 0 4px 16px rgba(0,0,0,.06); white-space: nowrap;
  transition: transform 0.4s var(--ease-soft), box-shadow 0.4s var(--ease-soft);
}
.fbadge:hover { transform: translateY(-3px) scale(1.04); box-shadow: 0 8px 20px rgba(37, 99, 235,.14); }
.fb1 { top: -14px;  right: -18px; }
.fb2 { bottom: 72px; left: -24px; }
.fb3 { bottom: -10px; right: 20px; }
.fbadge .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 7px; vertical-align: middle; }
.dot-blue{background:var(--blue);} .dot-purple{background:#8b5cf6;} .dot-green{background:#10b981;}


.marquee-wrap { position: relative; overflow: hidden; opacity: 0; animation: fadeUp .6s .65s var(--ease-soft) forwards; background: var(--surface); padding: 20px 0; border-top: 1px solid var(--border); }
.marquee-wrap::before, .marquee-wrap::after { content: ''; position: absolute; top: 0; bottom: 0; width: 100px; z-index: 2; pointer-events: none; }
.marquee-wrap::before { left:0; background: transparent; }
.marquee-wrap::after  { right:0; background: transparent; }
.marquee-track { display: flex; width: max-content; animation: marquee 25s linear infinite; }
.marquee-track:hover { animation-play-state: paused; }
@keyframes marquee { from{transform:translateX(0);} to{transform:translateX(-50%);} }

.si-esp32:hover    { color:#2563eb; background:#EAF2FC; box-shadow:0 0 14px rgba(37, 99, 235,.18); }
.si-arduino:hover  { color:#059669; background:#EDFAF4; box-shadow:0 0 14px rgba(5,150,105,.18); }
.si-firebase:hover { color:#D97706; background:#FEF3C7; box-shadow:0 0 14px rgba(217,119,6,.18); }
.si-react:hover    { color:#0EA5E9; background:#E0F2FE; box-shadow:0 0 14px rgba(14,165,233,.18); }
.si-python:hover   { color:#7C3AED; background:#F3EEFF; box-shadow:0 0 14px rgba(124,58,237,.18); }
.si-tf:hover       { color:#EA580C; background:#FFF7ED; box-shadow:0 0 14px rgba(234,88,12,.18); }
.si-opencv:hover   { color:#DC2626; background:#FEF2F2; box-shadow:0 0 14px rgba(220,38,38,.18); }
.si-yolo:hover     { color:#059669; background:#EDFAF4; box-shadow:0 0 14px rgba(5,150,105,.18); }
.si-figma:hover    { color:#DB2777; background:#FCE7F3; box-shadow:0 0 14px rgba(219,39,119,.18); }
.si-mqtt:hover     { color:#2563eb; background:#EAF2FC; box-shadow:0 0 14px rgba(37, 99, 235,.18); }

@media (max-width: 1024px) {
  .hero { flex-direction: column; text-align: center; justify-content: center; padding-top: 40px; }
  .hero-right { display: none; }
  .hero-sub { margin-left: auto; margin-right: auto; }
  .terminal-box { margin: 0 auto; }
  .hero-socials { justify-content: center; }
  .skill-pills { justify-content: center; }
}

/* ─── ABOUT PAGE ────────────────────────────────── */
.wrap, .wrap-about { max-width: 960px; margin: 0 auto; padding: 64px 5vw 100px; }
.intro {
  display: grid; grid-template-columns: 1fr auto; gap: 56px; align-items: center; margin-bottom: 32px;
  opacity: 0; animation: fadeUp .6s .1s var(--ease-soft) forwards;
}
.intro-mono { font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--blue); letter-spacing: .08em; margin-bottom: 16px; }
.intro-name { font-size: clamp(36px, 5vw, 60px); font-weight: 900; letter-spacing: -0.04em; line-height: 1.05; margin-bottom: 24px; color: var(--text-primary); }
.intro-name .line2 { color: var(--blue); display: block; }
.intro-bio { font-size: 16px; color: var(--text-secondary); line-height: 1.8; max-width: 500px; }
.status-pill {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 24px;
  font-size: 12px; font-weight: 700; color: #059669;
  background: #ecfdf5; padding: 8px 16px; border-radius: 99px; border: 1px solid #a7f3d0;
}
.status-pill .sdot {
  width: 6px; height: 6px; border-radius: 50%; background: #059669;
}

.intro-photo-wrap {
  position: relative;
  width: clamp(200px, 22vw, 300px);
  background: var(--surface-1);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1.5px solid rgba(200, 220, 255, 0.55);
  border-radius: 28px;
  padding: 14px;
  box-shadow:
    0 16px 48px rgba(37, 99, 235, 0.10),
    0 4px 16px rgba(0, 0, 0, 0.05),
    inset 0 1px 0 rgba(255, 255, 255, 0.85);
  transition: transform 0.45s var(--ease-soft), box-shadow 0.45s var(--ease-soft);
  flex-shrink: 0;
}
.intro-photo-wrap:hover {
  transform: translateY(-6px) rotate(-0.8deg);
  box-shadow:
    0 28px 72px rgba(37, 99, 235, 0.16),
    0 8px 24px rgba(0, 0, 0, 0.08),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);
}

/* Hapus old before/after shapes */
.intro-photo-wrap::before, .intro-photo-wrap::after { display: none; }

.intro-photo {
  position: relative; z-index: 1; width: 100%; aspect-ratio: 3/4; height: auto;
  object-fit: cover; object-position: 65% 15%; border-radius: 18px; display: block;
  transition: transform 0.5s ease; filter: saturate(1.05);
}
.intro-photo-wrap:hover .intro-photo { transform: scale(1.02); }

.sec { margin-bottom: 56px; opacity: 0; animation: fadeUp .6s var(--ease-soft) forwards; }
.sec:nth-child(1){animation-delay:.15s} .sec:nth-child(2){animation-delay:.25s} .sec:nth-child(3){animation-delay:.35s} .sec:nth-child(4){animation-delay:.45s}

.sec-label {
  font-size: 11px; font-weight: 700; letter-spacing: .16em; color: var(--text-secondary); text-transform: uppercase; margin-bottom: 24px; display: flex; align-items: center; gap: 16px;
}
.sec-label::after { content:''; flex:1; height:1px; background:var(--border); }

.facts { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: 16px !important; }
@media (max-width: 520px) { .facts { grid-template-columns: 1fr !important; } }
.fact {
  position: relative; padding: 16px 20px; border: 1px solid var(--glass-border); border-radius: 12px; background: var(--surface);
  display: flex; align-items: center; gap: 16px; transition: all 0.3s var(--ease-soft);
 backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); }
.fact:hover { border-color: #cbd5e1; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.02); }
.fact-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.fact-key { font-size: 11px; font-weight: 700; color: var(--text-secondary); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 4px; }
.fact-val { font-size: 14px; font-weight: 600; color: var(--text-primary); }

/* Ikon watermark: mengisi ruang kosong pada card dengan teks pendek
   (Location, Status) supaya seimbang dengan card teks panjang (Birthday,
   University), tanpa perlu mengubah grid 2 kolom yang sudah rapi */
.fact { overflow: hidden; }
.fact::after {
  content: ''; position: absolute; top: 50%; right: -10px;
  width: 56px; height: 56px; transform: translateY(-50%) rotate(-10deg);
  background: var(--fact-accent, #94a3b8); opacity: .07;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  pointer-events: none;
}
.fact--birthday { --fact-accent: #4A90D9; }
.fact--birthday::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='5' width='18' height='15' rx='2'/%3E%3Crect x='6' y='2' width='2' height='5' rx='1'/%3E%3Crect x='16' y='2' width='2' height='5' rx='1'/%3E%3Crect x='3' y='9' width='18' height='2'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='5' width='18' height='15' rx='2'/%3E%3Crect x='6' y='2' width='2' height='5' rx='1'/%3E%3Crect x='16' y='2' width='2' height='5' rx='1'/%3E%3Crect x='3' y='9' width='18' height='2'/%3E%3C/svg%3E");
}
.fact--location { --fact-accent: #2EA87E; }
.fact--location::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2C8 2 5 5.1 5 9c0 5.2 7 13 7 13s7-7.8 7-13c0-3.9-3-7-7-7zm0 10a3 3 0 110-6 3 3 0 010 6z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2C8 2 5 5.1 5 9c0 5.2 7 13 7 13s7-7.8 7-13c0-3.9-3-7-7-7zm0 10a3 3 0 110-6 3 3 0 010 6z'/%3E%3C/svg%3E");
}
.fact--university { --fact-accent: #7C3AED; }
.fact--university::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3 2 8l10 5 10-5z'/%3E%3Cpath d='M6 12.5v4c0 1.7 2.7 3 6 3s6-1.3 6-3v-4l-6 3z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3 2 8l10 5 10-5z'/%3E%3Cpath d='M6 12.5v4c0 1.7 2.7 3 6 3s6-1.3 6-3v-4l-6 3z'/%3E%3C/svg%3E");
}
.fact--status { --fact-accent: #D97706; }
.fact--status::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 4 5v6c0 5 3.4 9 8 11 4.6-2 8-6 8-11V5z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 4 5v6c0 5 3.4 9 8 11 4.6-2 8-6 8-11V5z'/%3E%3C/svg%3E");
}

/* ── LET'S CONNECT ── */
.connect-panel { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; align-items: stretch; }
.connect-info { display: flex; flex-direction: column; }
.connect-actions { margin-top: auto; }

.connect-title { font-size: clamp(26px, 3.2vw, 38px); font-weight: 900; letter-spacing: -0.03em; line-height: 1.14; color: var(--text-primary); margin-bottom: 14px; }
.connect-title span { color: var(--blue); }
.connect-desc { font-size: 15px; color: var(--text-secondary); line-height: 1.75; max-width: 420px; margin-bottom: 26px; }

.connect-socials { margin-top: 0; margin-bottom: 26px; animation: none; opacity: 1; }
.connect-socials a { width: 42px; height: 42px; }

.connect-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.cv-btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 11px 20px; border-radius: 99px;
  font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
  color: #fff; text-decoration: none; border: none; cursor: pointer;
  background: linear-gradient(135deg, var(--blue) 0%, #7c3aed 100%);
  box-shadow: 0 8px 20px rgba(37, 99, 235,.22);
  transition: transform .3s var(--ease-soft), box-shadow .3s var(--ease-soft);
}
.cv-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 26px rgba(37, 99, 235,.3); }
.cv-btn:active { transform: translateY(0); }

.connect-form {
  background: var(--surface); border: 1px solid var(--glass-border); border-radius: 22px;
  padding: 26px; display: flex; flex-direction: column; gap: 16px;
}
.cf-field { display: flex; flex-direction: column; gap: 7px; }
.cf-field label { font-size: 10.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--text-secondary); }
.cf-field input, .cf-field textarea {
  width: 100%; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 14px; color: var(--text-primary);
  background: var(--bg-page); border: 1px solid var(--glass-border); border-radius: 12px; padding: 12px 14px;
  resize: vertical; transition: border-color .25s var(--ease-soft), box-shadow .25s var(--ease-soft);
}
.cf-field textarea { min-height: 108px; line-height: 1.6; }
.cf-field input::placeholder, .cf-field textarea::placeholder { color: var(--text-tertiary); }
.cf-field input:focus, .cf-field textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-lt); }

.cf-submit {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px; margin-top: 2px;
  padding: 14px 22px; border: none; border-radius: 14px; cursor: pointer;
  font-family: inherit; font-size: 13.5px; font-weight: 700; color: #fff;
  background: linear-gradient(135deg, var(--blue) 0%, #7c3aed 100%);
  box-shadow: 0 10px 24px rgba(37, 99, 235,.25);
  transition: transform .3s var(--ease-soft), box-shadow .3s var(--ease-soft);
}
.cf-submit:hover { transform: translateY(-2px); box-shadow: 0 14px 30px rgba(37, 99, 235,.32); }
.cf-submit:active { transform: translateY(0); }
.cf-submit:disabled { opacity: .6; cursor: default; transform: none; }

.cf-status { font-size: 12px; font-weight: 600; color: var(--text-secondary); min-height: 16px; margin: 0; }
.cf-status.is-ok { color: #059669; }
.cf-status.is-error { color: #dc2626; }

@media (max-width: 860px) {
  .connect-panel { grid-template-columns: 1fr; gap: 30px; }
}

.connect-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
.connect-item {
  position: relative; display: flex; align-items: center; gap: 16px; padding: 16px 20px; border-radius: 12px; background: var(--surface); border: 1px solid var(--glass-border); text-decoration: none; color: var(--text-primary); transition: all 0.3s var(--ease-soft);
}
.connect-item:hover { border-color: var(--blue); transform: translateY(-4px); box-shadow: 0 8px 20px rgba(37, 99, 235,0.06); }
.connect-platform { font-size: 14px; font-weight: 700; flex: 1; color: var(--text-primary); }
.connect-handle { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text-secondary); transition: color 0.3s; }
.connect-item:hover .connect-handle { color: var(--blue); }
.connect-arr { font-size: 16px; color: #cbd5e1; transition: color 0.3s, transform 0.3s; }
.connect-item:hover .connect-arr { color: var(--blue); transform: translateX(4px); }

@media (max-width: 768px) {
  .intro { grid-template-columns: 1fr; text-align: center; gap: 32px; }
  .intro-photo-wrap, .intro-bio, .status-pill { margin: 0 auto; }
}

/* ─── WORKS: CARDS & GRID ───────────────────────── */
.projects-wrap { padding: 32px 0 64px; width: 100%; flex: 1; overflow: hidden; }
.projects-grid { display: flex !important; gap: 40px !important; padding: 20px 5vw 40px !important; overflow-x: auto !important; overflow-y: hidden !important; scroll-snap-type: none !important; scroll-behavior: auto !important; scrollbar-width: none !important; }






.card { border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--glass-border); transition: transform 0.4s var(--ease-soft), box-shadow 0.4s var(--ease-soft); cursor: pointer; display: flex; flex-direction: column; opacity: 0; backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); box-shadow: 0 8px 24px rgba(0,0,0,0.04); }
.card:hover { transform: translateY(-8px); box-shadow: 0 20px 40px rgba(0,0,0,0.06); border-color: var(--glass-border-hi); }

.card-thumb { height: 220px; position: relative; overflow: hidden; flex-shrink: 0; background: var(--surface-2); border-radius: 16px 16px 0 0; }
.card-thumb > img { width: 100%; height: 100%; object-fit: contain; object-position: center; transition: transform 0.6s var(--ease-soft), filter 0.6s var(--ease-soft); filter: grayscale(100%); }
.card:hover .card-thumb > img { transform: scale(1.05); filter: grayscale(0%); }

/* Slideshows, Thumbnails & Controls */
.card-slideshow { position: relative; width: 100%; height: 100%; background: var(--surface-2); }
.card-slide { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.6s ease, filter 0.6s var(--ease-soft); pointer-events: none; filter: grayscale(100%); }
.card-slide.active { opacity: 1; pointer-events: auto; }
.card:hover .card-slide { filter: grayscale(0%); }

.cs-arr {
  position: absolute; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; border-radius: 50%; background: var(--surface-1); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(4px); color: var(--text-primary); border: 1px solid rgba(0,0,0,0.1); font-size: 16px; cursor: pointer; z-index: 5; display: flex; align-items: center; justify-content: center; transition: all 0.3s var(--ease-soft); opacity: 0;
}
.card-thumb:hover .cs-arr, .card-photo:hover .cs-arr { opacity: 1; }
.cs-arr:hover { background: var(--surface); transform: translateY(-50%) scale(1.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.cs-prev { left: 8px; } .cs-next { right: 8px; }

.cs-dots { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 6px; z-index: 5; }
.cs-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(0,0,0,0.2); border: none; cursor: pointer; padding: 0; transition: all 0.3s var(--ease-soft); }
.cs-dot.active { background: var(--dark); width: 16px; border-radius: 3px; }
.cs-counter { position: absolute; top: 8px; left: 10px; background: rgba(0,0,0,0.4); color: #fff; font-family: 'JetBrains Mono', monospace; font-size: 10px; padding: 2px 8px; border-radius: 6px; z-index: 5; backdrop-filter: blur(4px); }

.thumb-placeholder { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: var(--surface-2); }
.thumb-mono { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 700; color: var(--text-secondary); text-align: center; white-space: pre-line; }

.ph-iot, .ph-ml, .ph-cv, .ph-web, .ph-design { background: var(--surface-2); }

.cat-tag { position: absolute; top: 12px; right: 12px; font-size: 10px; font-weight: 700; padding: 6px 12px; border-radius: 99px; letter-spacing: .05em; z-index: 6; backdrop-filter: blur(6px); background: var(--surface-1); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: var(--text-primary); border: 1px solid rgba(0,0,0,0.05); text-transform: uppercase; }
.cat-iot { color: #1d4ed8; } .cat-ml { color: #6D28D9; } .cat-cv { color: #047857; } .cat-web { color: #B45309; } .cat-design { color: #BE185D; }
.accent-bar { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; z-index: 5; background: var(--blue); }

.bar-iot { background: #2563eb; } .bar-ml { background: #7C3AED; } .bar-cv { background: #059669; } .bar-web { background: #D97706; } .bar-design { background: #DB2777; }

.card-body { padding: 24px; flex: 1; display: flex; flex-direction: column; }
.card-title { font-size: 18px; font-weight: 800; margin-bottom: 8px; letter-spacing: -0.02em; line-height: 1.3; color: var(--text-primary); }
.card-desc  { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 24px; flex: 1; }
.card-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: auto; }
.card-stack  { display: flex; gap: 6px; flex-wrap: wrap; }
.stack-tag { font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 6px; background: var(--surface-2); color: var(--text-secondary); }
.stag-iot { background: #eff6ff; color: #2563eb; } .stag-ml { background: #f5f3ff; color: #7c3aed; } .stag-cv { background: #ecfdf5; color: #059669; } .stag-web { background: #fffbeb; color: #d97706; } .stag-design { background: #fdf2f8; color: #db2777; }

.card-link { font-size: 13px; font-weight: 700; color: var(--blue); text-decoration: none; white-space: nowrap; flex-shrink: 0; padding: 8px 16px; border-radius: 99px; background: var(--blue-lt); opacity: 0; transform: translateX(-10px); transition: all 0.3s var(--ease-soft); }
.card:hover .card-link { opacity: 1; transform: translateX(0); }
.card-link:hover { background: #bfdbfe; }

/* ─── MODAL (Works) ─────────────────────────────── */
.modal-overlay { display: none; position: fixed; inset: 0; z-index: 999; background: rgba(15, 23, 42, 0.4); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); align-items: center; justify-content: center; }
.modal-overlay.open { display: flex; animation: fadeIn .3s var(--ease-soft); }
.modal { background: var(--surface); border-radius: 24px; width: 92%; max-width: 720px; max-height: 90vh; overflow-y: auto; position: relative; animation: scaleIn .4s var(--ease-soft); box-shadow: 0 24px 64px rgba(0,0,0,0.1); }
.modal-close { position: absolute; top: 16px; right: 20px; z-index: 20; background: var(--surface-1); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(0,0,0,0.1); border-radius: 50%; width: 36px; height: 36px; font-size: 18px; color: var(--text-primary); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s var(--ease-soft); backdrop-filter: blur(4px); }
.modal-close:hover { background: var(--dark); color: #fff; transform: scale(1.1); }

.modal-slideshow { position: relative; height: 400px; overflow: hidden; border-radius: 24px 24px 0 0; background: var(--surface-2); }
.modal-slideshow > img { width: 100%; height: 100%; object-fit: contain; }
.lb-slide.active { opacity: 1; } 
.lb-dot.active { background: var(--dark); width: 20px; border-radius: 4px; }

.modal-body { padding: 32px; }
.modal-cat-tag { display: inline-block; font-size: 11px; font-weight: 700; padding: 6px 14px; border-radius: 99px; margin-bottom: 16px; background: var(--blue-lt); color: var(--blue); text-transform: uppercase; letter-spacing: 0.05em; }
.modal-title { font-size: 28px; font-weight: 900; letter-spacing: -0.03em; margin-bottom: 12px; line-height: 1.2; color: var(--text-primary); }
.modal-desc  { font-size: 15px; color: var(--text-secondary); line-height: 1.7; margin-bottom: 24px; }
.modal-meta-row { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 16px; }
.modal-meta-label { font-size: 11px; font-weight: 700; color: var(--text-secondary); letter-spacing: .1em; text-transform: uppercase; min-width: 80px; padding-top: 4px; }
.modal-link { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; color: var(--blue); text-decoration: none; padding: 10px 20px; border-radius: 99px; background: var(--blue-lt); transition: background 0.3s var(--ease-soft); }
.modal-link:hover { background: #bfdbfe; }

/* ─── CERTIFICATE BOARD (frame wall) + shared board/count chrome ─── */
.board-wrap { padding: 16px 5vw 80px; max-width: 1280px; margin: 0 auto; width: 100%; }
.board { background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 40px 28px 28px; position: relative; box-shadow: 0 4px 24px rgba(0,0,0,0.02); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); }
.board-label { position: absolute; top: 14px; left: 24px; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--text-secondary); text-transform: uppercase; }

.act-count, .cert-count { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text-secondary); border: 1px solid var(--glass-border); background: var(--surface); padding: 8px 16px; border-radius: 12px; white-space: nowrap; flex-shrink: 0; }
.act-count span, .cert-count span { color: var(--blue); font-weight: 700; }


/* ─── CERTIFICATE PAGE — GALLERY WALL ───────────────────── */
.pin-grid { columns: 3; column-gap: 44px; padding-top: 8px; }
@media (max-width: 1024px) { .pin-grid { columns: 2; column-gap: 32px; } }
@media (max-width: 640px) { .pin-grid { columns: 1; padding-top: 0; } }

.pin-card {
  break-inside: avoid;
  margin-bottom: 8px;
  position: relative;
  opacity: 0;
  cursor: pointer;
}
.pin-card.hidden { display: none; }

/* Wrapper terpisah untuk rotasi salon-style, supaya tidak bentrok dengan animasi
   masuk halaman (slideLeftIn / reveal) yang menyasar .pin-card langsung */
.frame-wrap {
  position: relative;
  transform-origin: 50% 100%;
  transition: transform .5s var(--ease-soft);
  will-change: transform;
}

/* Kemiringan & pergeseran acak ala "salon hang" — tumpang tindih halus antar bingkai */
.pin-card:nth-child(6n+1) .frame-wrap { transform: rotate(-3deg)  translate(6px, 8px);    z-index: 1; }
.pin-card:nth-child(6n+2) .frame-wrap { transform: rotate(2.2deg) translate(-10px, -16px); z-index: 2; }
.pin-card:nth-child(6n+3) .frame-wrap { transform: rotate(-1.4deg) translate(4px, -6px);   z-index: 3; }
.pin-card:nth-child(6n+4) .frame-wrap { transform: rotate(2.8deg)  translate(-6px, 12px);  z-index: 2; }
.pin-card:nth-child(6n+5) .frame-wrap { transform: rotate(-2.2deg) translate(9px, -10px);  z-index: 3; }
.pin-card:nth-child(6n)   .frame-wrap { transform: rotate(1.3deg)  translate(-5px, 6px);   z-index: 1; }
@media (max-width: 640px) {
  .pin-card .frame-wrap { transform: rotate(0deg) translate(0,0) !important; }
}

.pin-card:hover .frame-wrap { transform: translateY(-10px) scale(1.045) !important; z-index: 30 !important; }

/* Bingkai minimalis — kaca tipis dengan aksen biru sesuai tema */
.cert-paper {
  border-radius: 10px;
  padding: 9px;
  position: relative;
  background: linear-gradient(155deg, rgba(255,255,255,.85), rgba(255,255,255,.55));
  border: 1px solid rgba(148,163,184,.4);
  box-shadow:
    0 14px 28px rgba(15,23,42,.09),
    0 3px 8px rgba(15,23,42,.05),
    inset 0 0 0 1px rgba(255,255,255,.7);
  transition: box-shadow .4s var(--ease-soft);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
}
.pin-card:hover .cert-paper { box-shadow: 0 28px 52px rgba(15,23,42,.16), 0 6px 14px rgba(15,23,42,.08), inset 0 0 0 1px rgba(255,255,255,.85); }

/* "Paku" kecil di atas bingkai, pengganti pin/tape lama */
.cert-paper::before {
  content: '';
  position: absolute;
  top: -13px; left: 50%;
  transform: translateX(-50%);
  width: 7px; height: 7px;
  border-radius: 50%;
  background: linear-gradient(155deg,#fff,#94a3b8);
  box-shadow: 0 2px 5px rgba(15,23,42,.3), 0 0 0 3px rgba(255,255,255,.55);
  z-index: 4;
}
/* "benang" tipis dari paku ke bingkai — sudut kiri & kanan */
.cert-paper::after {
  content: '';
  position: absolute;
  top: -11px; left: 18%;
  width: 64%; height: 12px;
  border: 1px solid rgba(148,163,184,.45);
  border-bottom: none;
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
  z-index: 3;
  pointer-events: none;
}

/* Kanvas / isi lukisan — mat dalam tipis warna biru */
.cert { width: 100%; background: var(--surface); border-radius: 5px; overflow: hidden; box-shadow: inset 0 0 0 1px rgba(37, 99, 235,.14); }

/* Plakat museum di bawah tiap bingkai */
.frame-plate {
  margin: 10px auto 0;
  max-width: 88%;
  padding: 7px 14px;
  border-radius: 10px;
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--border);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 4px 12px rgba(0,0,0,.03);
}
.frame-plate-title {
  font-size: 11px; font-weight: 700; color: var(--text-primary); letter-spacing: .02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.frame-plate-meta {
  font-size: 9.5px; color: var(--text-secondary); font-family: 'JetBrains Mono', monospace;
  letter-spacing: .06em; text-transform: uppercase; margin-top: 2px;
}
.cert-banner { height: 6px; background: var(--blue); }
.cert-body { padding: 24px 28px; position: relative; }
.cert-photo { width: 100%; display: block; object-fit: cover; max-height: 180px; border-bottom: 1px solid var(--border); transition: filter 0.6s var(--ease-soft); filter: grayscale(100%); }
.pin-card:hover .cert-photo { filter: grayscale(0%); }
.cert-photo-footer { padding: 20px 24px; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }

.cert-platform { font-size: 11px; font-weight: 700; letter-spacing: .12em; color: var(--text-secondary); text-transform: uppercase; margin-bottom: 8px; }
.cert-recipient { font-family: 'Inter', sans-serif; font-size: 20px; font-weight: 800; color: var(--text-primary); margin-bottom: 8px; letter-spacing: -0.02em; }
.cert-course { font-size: 14px; font-weight: 600; color: var(--text-secondary); line-height: 1.5; margin-bottom: 20px; text-transform: none; }
.cert-date { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text-secondary); font-weight: 500; }
.cert-divider { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
.cert-footer-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.cert-badges { display: flex; gap: 6px; flex-wrap: wrap; }
.cert-badge { font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 6px; letter-spacing: .04em; }

.badge-iot { background: #eff6ff; color: #2563eb; } .badge-ml { background: #f5f3ff; color: #7c3aed; } .badge-cv { background: #ecfdf5; color: #059669; } .badge-web { background: #fffbeb; color: #d97706; } .badge-design { background: #fdf2f8; color: #db2777; } .badge-cloud { background: #f0fdf4; color: #15803d; } .badge-course { background: #eff6ff; color: #0284c7; } .badge-training { background: #fffbeb; color: #b45309; } .badge-compete { background: #fff1f2; color: #e11d48; } .badge-internship { background: #faf5ff; color: #9333ea; } .badge-org { background: #f0fdf4; color: #16a34a; }

.cert-seal { position: absolute; bottom: 16px; right: 20px; width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 800; text-align: center; line-height: 1.2; border: 2px solid; opacity: .3; }

.cert-photo-name { font-size: 13px; font-weight: 700; color: var(--text-primary); line-height: 1.4; margin-bottom: 4px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.cert-photo-platform { font-size: 11px; font-weight: 600; color: var(--text-secondary); letter-spacing: .08em; text-transform: uppercase; line-height: 1.4; }
.cert-photo-badges { display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }

#lightbox-content .cert-banner { height: 8px; }
#lightbox-content .cert-body { padding: 32px 36px; }
#lightbox-content .cert-platform { font-size: 13px; margin-bottom: 12px; }
#lightbox-content .cert-recipient { font-size: 28px; margin-bottom: 16px; }
#lightbox-content .cert-course { font-size: 18px; margin-bottom: 24px; }
#lightbox-content .cert-date { font-size: 14px; }
#lightbox-content .cert-badge { font-size: 13px; padding: 6px 14px; }
#lightbox-content .cert-seal { width: 64px; height: 64px; font-size: 12px; }

#lightbox-content .cert-photo-name { font-size: 18px; -webkit-line-clamp: unset; }
#lightbox-content .cert-photo-platform { font-size: 13px; -webkit-line-clamp: unset; }
#lightbox-content .cert-photo-badges { flex-direction: row; }

/* ─── ANIMATIONS & SKELETONS ────────────────────── */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes scaleIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }

.shimmer { background: transparent; background-size: 200% 100%; animation: shimmer 1.5s infinite linear; }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

.skeleton-item, .skeleton-pin-card { border-radius: 16px; background: var(--surface); border: 1px solid var(--border); overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,.02); }

/* Utilities */
.hr { border: none; border-top: 1px solid var(--border); margin: 0 0 32px; }
.state-msg { text-align: center; padding: 80px 0; font-size: 14px; color: var(--text-secondary); width: 100%; font-weight: 500; }
.state-msg.error { color: #ef4444; }


/* AI Dashboard Hover Focus */
.projects-grid .card { 
    flex: 0 0 calc(85vw - 40px) !important; max-width: 600px !important; height: 100% !important; scroll-snap-align: center !important;
    border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--glass-border); display: flex; flex-direction: column; opacity: 1; backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); 
    transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; position: relative;
    box-shadow: 0 8px 24px rgba(0,0,0,0.04);
}

.projects-grid:hover .card {
    opacity: 0.6;
    filter: blur(4px) brightness(0.8);
    transform: scale(0.97);
}

.projects-grid .card:hover {
    opacity: 1 !important;
    filter: blur(0px) brightness(1) !important;
    transform: scale(1.05) translateY(-8px) !important;
    border-color: #8b5cf6;
    box-shadow: 0 16px 40px rgba(0,0,0,0.1), 0 0 30px rgba(139, 92, 246, 0.4), 0 0 15px rgba(37, 99, 235, 0.4);
    z-index: 10;
}
.projects-grid .card::before {
    content: ''; position: absolute; inset: 0; border-radius: 16px; pointer-events: none;
    box-shadow: inset 0 0 20px rgba(139, 92, 246, 0); transition: box-shadow 0.35s ease;
}
.projects-grid .card:hover::before {
    box-shadow: inset 0 0 20px rgba(139, 92, 246, 0.2);
}

.projects-grid::-webkit-scrollbar { display: none !important; }
.projects-grid::after { content: ""; flex: 0 0 5vw; }


/* ─── CardMorph shared "grows from the clicked card" pop-up (premium.js) ───
   Added dynamically, only on the exact panel element being morphed, for
   the duration of the GSAP Flip tween — kills the CSS opacity/transform
   transition on that instance so it can't fight the transform GSAP is
   writing every frame (only .lightbox-inner inside #act-lightbox ever
   gets this class right now; #lightbox's Certificate instance and the
   Works .fan-detail panel are untouched, plain CSS fade+scale). */
.flip-lock,
.flip-lock.lightbox-inner {
  transition: none !important;
}

/* ─── LIGHTBOX (Activities & Certificate) ─── */
.lightbox {
  display: flex;
  /* justify-content here is the VERTICAL axis (flex-direction: column).
     "safe center" centers .lightbox-inner in the viewport when it's
     short (fixes it looking pinned/stranded near the top for entries
     with little content, like ones with no photo) but automatically
     falls back to top-anchored + scrollable once content is taller
     than the viewport, so nothing at the top of a long panel becomes
     unreachable — the exact "Flexbox clipping" the old block+margin
     approach was working around. Browsers without `safe` support just
     ignore the value and behave like `flex-start` (top-anchored,
     scrollable) — the previous, still-safe, behavior. */
  flex-direction: column;
  align-items: center;
  justify-content: safe center;
  position: fixed; 
  top: 0; left: 0; right: 0; bottom: 0; 
  width: 100vw; height: 100vh;
  z-index: 99999;
  background: rgba(15, 23, 42, 0.4);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  opacity: 0; visibility: hidden; pointer-events: none;
  /* Matches CardMorph's Flip duration (0.6s open / 0.45s close, see
     premium.js) so the backdrop darkens over the same span the panel
     is still traveling, instead of finishing ~0.3s early and making
     the morph look like it "catches up" to an already-dark screen. */
  transition: opacity 0.6s ease, visibility 0.6s ease;
  overflow-y: auto; 
  overflow-x: hidden;
  padding: 60px 16px; /* replaces .lightbox-inner's old top/bottom margin hack */
  scrollbar-width: none; /* Firefox — still scrollable, just no visible track */
  -ms-overflow-style: none; /* old Edge/IE */
}
.lightbox::-webkit-scrollbar { display: none; width: 0; height: 0; }

.lightbox.open {
  opacity: 1; visibility: visible; pointer-events: auto;
}

.lightbox-inner {
  background: var(--surface-1);
  backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
  border: 1px solid rgba(0,0,0,0.05);
  border-radius: 20px;
  width: 100%; 
  max-width: 720px; 
  height: auto; 
  max-height: calc(100vh - 120px); /* selalu muat di viewport (dikurangi padding 60px atas/bawah .lightbox) */
  display: flex;
  flex-direction: column;
  /* Panel ini sendiri TETAP tidak men-scroll — yang men-scroll adalah
     #lightbox-content di dalamnya. Alasannya: .lightbox-close adalah
     anak elemen ini, dan karena elemen ini punya `transform`, dia jadi
     containing block untuk si tombol. Kalau panelnya yang di-scroll,
     tombol close-nya ikut tergulung naik dan hilang dari layar. */
  overflow: hidden;
  position: relative; 
  margin: auto; /* horizontal centering fallback + vertical breathing room when flex can't safe-center */
  flex-shrink: 0; /* never let the flex column squash a tall panel */
  box-shadow: 0 40px 100px rgba(0,0,0,0.4);
  transform: translateY(30px) scale(0.97);
  transition: all 0.4s var(--ease-soft);
}

.lightbox.open .lightbox-inner {
  transform: translateY(0) scale(1);
}

.lightbox-close {
  position: fixed !important; top: 32px !important; right: 32px !important; z-index: 999999 !important;
  background: #0f172a; border: 2px solid white; border-radius: 50%;
  width: 40px; height: 40px; font-size: 18px; color: white;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: all 0.3s ease;
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.lightbox-close:hover { background: #e11d48; transform: scale(1.1); }

/* CONTENT RESETS */
/* INI area yang men-scroll saat isi sertifikat lebih tinggi dari panel.
   Dulu `overflow: hidden` — jadi begitu flexbox memampatkan elemen ini
   supaya muat di .lightbox-inner yang ber-max-height, kelebihannya
   dipotong diam-diam dan tidak bisa dijangkau sama sekali. Itu penyebab
   langsung deskripsi sertifikat terpangkas di bawah.

   `min-height: 0` WAJIB ada: secara default flex item TIDAK MAU menyusut
   di bawah tinggi min-content-nya, jadi tanpa ini overflow-y:auto tidak
   pernah aktif dan pemotongannya tetap terjadi. Ini kunci yang gampang
   terlewat. */
#lightbox-content {
  width: 100%; position: relative; margin: 0; padding: 0;
  border-radius: 20px;
  overflow-x: hidden;
  overflow-y: auto;
  min-height: 0;
  overscroll-behavior: contain; /* scroll di dalam popup tidak merembet ke halaman */
  scrollbar-width: thin;
}
#lightbox-content .cert { background: transparent !important; border: none !important; box-shadow: none !important; width: 100%; }
#lightbox-content .cert-banner { height: 8px; width: 100%; }
#lightbox-content .cert-recipient, #lightbox-content .cert-course, #lightbox-content .cert-platform, #lightbox-content .cert-photo-name { color: var(--text-primary) !important; }

/* FIX IMAGE RENDERING AND GRAYSCALE */
#lightbox-content .cert-photo {
  filter: grayscale(0%) !important; /* CRITICAL: Force original colors */
  object-fit: contain !important; 
  height: auto !important;
  max-height: 70vh !important; /* Huge but fits on screen */
  background: rgba(0,0,0,0.04);
}

.cert-pdf-link { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; color: #fff !important; background: var(--blue) !important; text-decoration: none; padding: 12px 24px; border-radius: 99px; margin: 0 28px 32px 28px; transition: all 0.3s ease; }
.cert-pdf-link:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(37, 99, 235,0.4); }

.cert-pdf-frame { margin: 0 12px 16px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; overflow: hidden; }
.cert-pdf-frame iframe { display: block; width: 100%; height: 55vh; border: 0; background: #fff; }


#lightbox-content * {
  filter: none !important; /* Nuke all filters, including grayscale */
}
#lightbox-content .cert-photo {
  /* Dulu `max-height: none !important`, yang membatalkan batas 70vh di
     atas — foto sertifikat portrait jadi setinggi apa pun, mendorong
     footer (judul + deskripsi) keluar dari panel. Dibatasi lagi, tapi
     lebih longgar dari 70vh supaya foto tetap besar sekaligus menyisakan
     ruang untuk teks di bawahnya. Sekarang isi popup umumnya muat tanpa
     perlu di-scroll sama sekali. */
  max-height: 62vh !important;
  height: auto !important;
  object-fit: contain !important;
  width: 100% !important;
}


.px-layer-fast {
  position: absolute;
  width: 40vw; height: 40vw;
  background: #DCD0FF; /* Lavender / Soft Purple */
  border-radius: 50%;
  filter: blur(60px);
  bottom: 5%; right: 5%;
  opacity: 0.7;
  transform: translate(0, 0);
  will-change: transform;
}


/* ── ACTIVITY LIGHTBOX STYLES (CLEANED) ── */
.lb-photo { width: 100%; aspect-ratio: 16 / 10; flex-shrink: 0; max-height: 48vh; border-radius: 20px 20px 0 0; overflow: hidden; background: #0f172a; position: relative; display: flex; align-items: center; justify-content: center; }
.lb-photo img { width: 100%; height: 100%; object-fit: cover; display: block; margin: 0 auto; }
.lb-body { padding: 32px; background: var(--surface); border-radius: 0 0 20px 20px; position: relative; overflow-y: auto; min-height: 0; scrollbar-width: none; -ms-overflow-style: none; }
.lb-body::-webkit-scrollbar { display: none; width: 0; height: 0; }
.lb-title { font-size: 24px; font-weight: 800; color: var(--text-primary); margin-bottom: 12px; line-height: 1.3; }
.lb-desc { font-size: 15px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 24px; }
.lb-meta { display: flex; align-items: center; gap: 16px; border-top: 1px solid var(--border); padding-top: 16px; }
.lb-date { font-size: 13px; font-weight: 500; color: var(--text-secondary); }

.lb-cat-tag {
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-secondary); background: var(--bg-page);
  border: 1px solid var(--glass-border); border-radius: 999px; padding: 4px 12px; margin-bottom: 12px;
}

/* Stack tags + project/GitHub links — Works items only, hidden (via
   [hidden]) for Activities. See openActLightbox in index.html. */
.lb-stack { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.lb-stack-tag {
  font-family: 'JetBrains Mono', monospace; font-size: 11.5px; font-weight: 600; color: var(--text-primary);
  background: var(--bg-page); border: 1px solid var(--glass-border); border-radius: 8px; padding: 4px 10px;
}
.lb-links { display: flex; gap: 10px; margin-top: 16px; }
.lb-link-btn {
  display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700;
  color: #fff; background: var(--blue, #2563eb); border-radius: 10px; padding: 9px 16px;
  text-decoration: none; transition: transform .15s ease, opacity .15s ease;
}
.lb-link-btn:hover { transform: translateY(-1px); opacity: .92; }
.lb-link-btn.lb-link-github { background: #0f172a; }

/* ════════════════════════════════════════════════════════════════
   LB STORY — isi #act-lightbox (Works & Activities), dibangun
   works-scrollstory.js. Dua bagian yang TERPISAH:

   1. HEADER STATIS (.lb-story-head) — "penjelasan awal" item, gaya
      kartu kertas: eyebrow kategori, judul serif, stack tags, baris
      meta (Kategori / Periode), deskripsi, tombol link. Tidak
      scroll-linked.
   2. SCROLL STORY (.lb-story-scroll) — di bawah header, latar gelap.
      Panel foto sticky yang crossfade + kolom "beat" (satu foto +
      judul/deskripsi khusus foto itu) yang discroll. Port vanilla
      komponen React "scroll-01".

   ≤640px: semua runtuh jadi tumpukan biasa (tanpa sticky).
   ════════════════════════════════════════════════════════════════ */
#act-lightbox {
  /* matikan bounce di layer overlay-nya juga (bukan cuma .lb-story) */
  overscroll-behavior: none;
}
#act-lightbox .lightbox-inner {
  height: min(720px, calc(100vh - 140px));
  max-width: 760px;
}
.lb-story-mount { flex: 1 1 auto; min-height: 0; display: flex; }

.lb-story {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  /* `none` (bukan `contain`): selain memutus scroll-chaining ke belakang,
     ini juga mematikan efek rubber-band / bounce saat scroll mentok
     paling atas atau paling bawah di dalam popup. */
  overscroll-behavior: none;
  background: transparent;
  border-radius: 20px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.lb-story::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* ── 1. HEADER STATIS ── */
.lb-story-head {
  flex: none;
  padding: clamp(28px, 4vw, 48px) clamp(24px, 4.5vw, 56px) clamp(26px, 3.5vw, 40px);
  background:
    radial-gradient(120% 90% at 12% 0%, rgba(255,255,255,.7), transparent 60%),
    linear-gradient(180deg, #f3efe6 0%, #ece7dc 100%);
  color: #2a2a2a;
  border-radius: 20px 20px 0 0;
}
.lb-story--headonly .lb-story-head { border-radius: 20px; }

.lb-story-eyebrow {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: #6b6357;
  margin: 0 0 14px;
}
.lb-story-eyebrow::after {
  content: "";
  height: 1px;
  flex: 1;
  background: rgba(0, 0, 0, .22);
}
.lb-story-title {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: clamp(25px, 3.4vw, 38px);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -.01em;
  color: #1c1c1c;
  margin: 0 0 18px;
}
.lb-story-head-stack {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
}
.lb-story-tag {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  color: #4a4034;
  background: rgba(255, 255, 255, .5);
  border: 1px solid rgba(0, 0, 0, .18);
  border-radius: 999px;
  padding: 5px 13px;
}

.lb-story-meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px 40px;
  max-width: 100%;
  padding-top: 0;
  margin-bottom: 22px;
  border-top: none;
}
.lb-story-meta[data-cols="1"] { grid-template-columns: minmax(0, 1fr); }
.lb-story-meta-cell { 
  display: flex; 
  flex-direction: column; 
  gap: 8px;
  padding-top: 18px;
  border-top: 1px solid rgba(0, 0, 0, .16);
}
.lb-story-meta-cell:first-child {
  border-top: 1px solid rgba(0, 0, 0, .16);
}
.lb-story-meta-val {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: clamp(17px, 2vw, 22px);
  font-weight: 600;
  color: #1c1c1c;
  order: 1;
  position: relative;
  padding-bottom: 0;
}
.lb-story-meta-val::after {
  display: none;
}
.lb-story-meta-label {
  order: 2;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: #8a8073;
  position: relative;
  padding-top: 9px;
}
.lb-story-meta-label::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 26px;
  height: 2px;
  background: #a8842b; /* aksen emas seperti referensi */
}
.lb-story-head-desc {
  font-size: clamp(14px, 1.5vw, 16px);
  line-height: 1.7;
  color: #4d4639;
  max-width: 62ch;
  margin: 0;
}
.lb-story-head-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 22px;
}

.lb-story-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  background: var(--blue, #2563eb);
  border-radius: 10px;
  padding: 9px 16px;
  text-decoration: none;
  transition: transform .15s ease, opacity .15s ease;
}
.lb-story-link:hover { transform: translateY(-1px); opacity: .92; }
.lb-story-link--github { background: #1c1c1c; }

/* ── 2. SCROLL STORY (latar gelap, terpisah di bawah) ──
   Dua kolom (foto sticky + teks) dirapatkan & di-center sebagai satu
   pasangan — bukan ditempel ke tepi kiri/kanan popup. Tiap beat setinggi
   fotonya dan teksnya di-center vertikal, jadi teks yang aktif selalu
   sejajar dengan foto (bukan mengambang jauh di ruang kosong). */
.lb-story-scroll {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 290px) minmax(0, 300px);
  justify-content: center;
  column-gap: clamp(14px, 2.4vw, 30px);
  align-items: start;
  /* jarak atas → foto pertama tidak nempel ke seam header */
  padding: clamp(44px, 10vh, 96px) clamp(16px, 4vw, 36px) 0;
  border-radius: 0 0 20px 20px;

  /* ── LATAR PLESTER NAVY ──
     Menggantikan `background: #121212` yang datar. Teksturnya
     DIBANGKITKAN dengan dua layer SVG feTurbulence sebagai data-URI,
     pendekatan yang sama dengan permukaan kertas di board Achievements
     — nol request jaringan, nol file aset, tetap tajam di
     devicePixelRatio berapa pun.

     Blend mode-nya berbeda dari board karena dasarnya GELAP: `overlay`
     dan `soft-light` bisa menggelapkan SEKALIGUS menerangkan, jadi
     hasilnya sapuan plester yang timbul-tenggelam. Kalau pakai
     `multiply` seperti board, teksturnya cuma menggelapkan navy yang
     memang sudah gelap — praktis tidak kelihatan apa-apa. */
  background-color: #1b2a45;
  background-image:
    /* Bintik/serat halus — DIULANG sebagai ubin 180px.

       `x='0' y='0' width='100%' height='100%'` di <filter>-nya WAJIB.
       Tanpa itu region filter default ke x=-10% y=-10% w=120% h=120%,
       jadi turbulensinya dibangkitkan pada area 20% LEBIH BESAR dari
       ubinnya — dan stitchTiles menyambung di batas region itu, bukan
       di batas ubin. Akibatnya tiap sambungan ubin kelihatan sebagai
       garis lurus membelah panel. Ini penyebab "garis kebelah" itu. */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='s' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23s)' opacity='0.22'/%3E%3C/svg%3E"),
    /* Sapuan plester kasar — TIDAK diulang sama sekali: background-size
       100% 100% merentangkan satu instance ke seluruh panel. Sapuan
       sebesar ini kalau diubin, pola berulangnya kelihatan jelas oleh
       mata walaupun sambungannya sudah mulus. Tidak ada ubin = tidak
       mungkin ada seam. */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='700' height='700'%3E%3Cfilter id='p' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.006 0.013' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='700' height='700' filter='url(%23p)' opacity='0.5'/%3E%3C/svg%3E"),
    /* sedikit gradasi cahaya dari kiri atas, biar panelnya tidak rata mati */
    radial-gradient(120% 100% at 15% 0%, rgba(120, 160, 220, 0.16), transparent 62%);
  background-size: 180px 180px, 100% 100%, 100% 100%;
  background-repeat: repeat, no-repeat, no-repeat;
  background-blend-mode: overlay, soft-light, normal;
}
.lb-story-scroll--single {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-bottom: 7vh;
}

.lb-story-media {
  position: sticky;
  /* `top` & padding kolom teks di-set presisi oleh works-scrollstory.js
     (layout()) supaya foto + teks yang aktif duduk pas di TENGAH panel.
     Nilai di bawah cuma fallback kalau JS mati. */
  top: 14vh;
  align-self: start;
  justify-self: end;
  width: 100%;
  max-width: 290px;
  height: min(50vh, 340px);
  margin: 0;
  border-radius: 18px;
  overflow: hidden;

  /* Putih, bukan #0f0f0f: di atas plester navy, kartu terang membuat
     foto benar-benar terbaca "mengambang" di depan dinding. Ini juga
     yang bikin logo/screenshot ber-background transparan tampil benar
     — di atas dasar hitam sebelumnya, logo gelap ikut hilang. */
  background: #fff;

  /* Shadow BERLAPIS, bukan satu shadow besar. Bayangan nyata punya dua
     komponen: kontak yang rapat & pekat tepat di bawah objek, dan
     ambient yang luas & lembut. Satu shadow saja cuma bisa jadi salah
     satunya — rapat tapi kaku, atau lebar tapi seperti kabut. Empat
     lapis dengan jarak menaik inilah yang membuat kartunya terbaca
     melayang, bukan sekadar ditempel. */
  box-shadow:
    0 2px 4px rgba(6, 12, 26, .30),
    0 8px 16px rgba(6, 12, 26, .28),
    0 22px 44px rgba(6, 12, 26, .34),
    0 44px 84px -20px rgba(6, 12, 26, .46),
    /* garis rambut terang di tepi — memisahkan kartu dari dinding
       gelap tanpa memakai border yang memakan ruang di dalam */
    0 0 0 1px rgba(255, 255, 255, .10);

  transform: translateZ(0); /* promosikan ke layer sendiri: shadow selebar ini mahal untuk di-repaint saat panel discroll */
}
.lb-story-scroll--single .lb-story-media {
  position: static;
  justify-self: auto;
  margin: 6vh 0 0;
  max-width: min(64%, 340px);
}

/* ── Mode "photos" — kalau tidak ada satu pun foto yang diberi
   judul/deskripsi di admin: JANGAN pakai 2 kolom (kolom teks bakal
   kosong & foto keliatan nempel ke kiri). Satu kolom saja: foto DI
   TENGAH panel, berganti seiring scroll. ── */
.lb-story-scroll--photos {
  display: block;
  padding: clamp(44px, 10vh, 96px) 0 0;
}
.lb-story-scroll--photos .lb-story-media {
  justify-self: auto;
  margin-left: auto;
  margin-right: auto;
  max-width: min(80%, 340px);
}
.lb-story-scroll--photos .lb-story-beats {
  width: 100%;
  padding-left: 0;
  padding-right: 0;
  gap: clamp(80px, 16vh, 200px); /* jarak scroll antar foto (tak ada teks) */
}
.lb-story-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  /* crossfade-nya di-ease per-frame di works-scrollstory.js (lerp),
     jadi TIDAK pakai CSS transition di sini biar tidak dobel/lag. */
  will-change: opacity;
}
.lb-story-layer.is-active { opacity: 1; }

/* Fallback kalau JS mati / prefers-reduced-motion: crossfade CSS biasa */
@media (prefers-reduced-motion: reduce) {
  .lb-story-layer { transition: opacity 0.2s linear; }
}
.lb-story-layer--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, .3);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.lb-story-beats {
  padding: 30vh 0; /* fallback — layout() nimpa dgn nilai presisi (½ ruang sisa) */
  display: flex;
  flex-direction: column;
  /* jarak antar beat digedein → butuh scroll lebih jauh utk pindah beat,
     jadi ada waktu baca teksnya */
  gap: clamp(150px, 22vh, 280px);
  text-align: left;
}
.lb-story-scroll--single .lb-story-beats {
  padding: 4vh 6% 0;
  gap: 0;
  max-width: 460px;
  text-align: center;
}
.lb-story-beat {
  will-change: opacity, transform;
  min-height: min(50vh, 340px); /* fallback — layout() nyamain dgn tinggi foto */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.lb-story-scroll--single .lb-story-beat { min-height: 0; opacity: 1 !important; transform: none !important; }

.lb-story-index {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: 'JetBrains Mono', monospace !important;
  font-size: 12px;
  font-weight: 700 !important;
  letter-spacing: .16em;
  color: rgba(255, 255, 255, .55);
}
.lb-story-index::before {
  content: "";
  width: 24px;
  height: 2px;
  background: #a8842b;
}
/* ── TIPOGRAFI BEAT ──
   Playfair sebenarnya SUDAH diminta di sini sejak awal, tapi tidak
   pernah tampil: aturan global `body, * { font-family:'DM Sans' …
   !important }` di L2517 mengalahkan semua deklarasi font biasa berapa
   pun spesifisitasnya. Itu sebabnya judul di popup ini selama ini
   ter-render sebagai sans tebal, bukan serif. !important di bawah yang
   mengembalikannya — pola yang sama dipakai semua override font lain
   di file ini. */
.lb-story-caption {
  font-family: 'Playfair Display', Georgia, serif !important;
  font-size: clamp(20px, 2.1vw, 27px);
  font-weight: 600 !important;
  line-height: 1.22;
  letter-spacing: -0.01em;
  color: #fff;
  margin: 0 0 12px;
}
.lb-story-caption + .lb-story-index,
.lb-story-index + .lb-story-caption { margin-top: 0; }

/* Deskripsi: Plus Jakarta Sans, bukan DM Sans bawaan. Pasangan
   serif-display + sans-humanis ini yang bikin judul dan penjelasan
   terbaca sebagai dua tingkat berbeda, bukan satu blok teks.

   Tiga penyesuaian khusus untuk teks terang di atas latar gelap:
   ukurannya dinaikkan sedikit, tracking dilonggarkan, dan warnanya
   TIDAK putih penuh (.78) — teks putih 100% di atas navy gelap
   "menyilaukan" dan bikin hurufnya terlihat menebal sendiri. */
.lb-story-sub {
  font-family: 'Plus Jakarta Sans', 'DM Sans', system-ui, sans-serif !important;
  font-size: 15px;
  font-weight: 400 !important;
  line-height: 1.7;
  letter-spacing: 0.005em;
  color: rgba(255, 255, 255, .78);
  max-width: 34ch;
  margin: 0;
}
.lb-story-scroll--single .lb-story-sub { margin: 0 auto; }

/* ── PETUNJUK SCROLL ──
   Scrollbar popup ini sengaja disembunyikan (.lb-story), dan foto
   pertama memenuhi panel — jadi tidak ada apa pun yang memberitahu
   bahwa isinya masih berlanjut. Pil ini yang memberitahu.

   JetBrains Mono dipilih supaya seirama sistem tipografi Achievements,
   di mana mono memang jadi "wajah label" (.page-eyebrow, .bg-eyebrow).
   Kebetulan .lb-story-index di popup yang sama juga sudah mono, jadi
   pilihan ini konsisten dari dua arah sekaligus.

   !important pada font-family: sama seperti teks lain di popup ini,
   aturan global `body, * { font-family:'DM Sans' … !important }` di
   L2517 akan menimpanya kalau tidak dipaksa. */
.lb-story-hint {
  position: sticky;
  bottom: 0;
  z-index: 6;
  align-self: center;
  flex: none;
  /* margin-top negatif: pil ini MELAYANG di atas ujung panel, bukan
     menambah tinggi area scroll. Kalau tidak, dia ikut memanjangkan
     konten dan malah menciptakan sedikit ruang yang bisa di-scroll
     walau isinya sudah habis. */
  margin: -56px 0 20px;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 17px;
  border-radius: 999px;

  font-family: 'JetBrains Mono', ui-monospace, monospace !important;
  font-size: 10.5px;
  font-weight: 700 !important;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.78);

  background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 8px 24px rgba(6, 12, 26, 0.35);

  pointer-events: none; /* murni penanda — jangan menghalangi klik/scroll di bawahnya */
  opacity: 1;
  transition: opacity .45s ease, transform .45s var(--ease-soft, ease);
}

/* line-height dikunci 1: JetBrains Mono punya line-height bawaan yang
   longgar, dan itu membuat pil lebih tinggi dari ikon panahnya sehingga
   teks dan panah tidak duduk sejajar. */
.lb-story-hint-label { line-height: 1; }

.lb-story-hint-arrow {
  width: 13px;
  height: 13px;
  flex: none;
  animation: lbHintBob 1.9s ease-in-out infinite;
}

@keyframes lbHintBob {
  0%, 100% { transform: translateY(-2px); opacity: .65; }
  50%      { transform: translateY(2px);  opacity: 1; }
}

/* Dilepas begitu user mulai men-scroll — lihat updateHint() di
   works-scrollstory.js. Dibiarkan di DOM (bukan display:none) supaya
   hilangnya ikut ter-transisi, bukan berkedip putus. */
.lb-story-hint.is-gone {
  opacity: 0;
  transform: translateY(10px);
}

@media (prefers-reduced-motion: reduce) {
  .lb-story-hint-arrow { animation: none; }
  .lb-story-hint { transition: opacity .2s linear; }
}

.lb-story-inline { display: none; }

/* ── GALLERY — foto tanpa judul/deskripsi, 2 kolom statis ── */
.lb-story-gallery {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(12px, 2vw, 20px);
  padding: clamp(24px, 4vh, 48px) clamp(16px, 4vw, 36px);
  background:
    radial-gradient(120% 90% at 12% 0%, rgba(255,255,255,.7), transparent 60%),
    linear-gradient(180deg, #f3efe6 0%, #ece7dc 100%);
}
.lb-story-gallery-col {
  display: flex;
  flex-direction: column;
  gap: clamp(12px, 2vw, 20px);
}
.lb-story-gallery-img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 12px;
  background: #0f0f0f;
}

@media (prefers-reduced-motion: reduce) {
  .lb-story-layer { transition: none; }
  .lb-story-beat { opacity: 1 !important; transform: none !important; }
}

@media (max-width: 860px) {
  .lb-story-scroll { grid-template-columns: minmax(0, 250px) minmax(0, 300px); }
  .lb-story-media { max-width: 250px; height: min(44vh, 300px); }
  .lb-story-beat { min-height: min(44vh, 300px); }
}

@media (max-width: 640px) {
  #act-lightbox .lightbox-inner { height: auto; max-height: calc(100vh - 80px); }
  .lb-story-head { border-radius: 16px 16px 0 0; }
  .lb-story-meta { grid-template-columns: minmax(0, 1fr); }
  .lb-story-scroll,
  .lb-story-scroll--single,
  .lb-story-scroll--photos { display: block; border-radius: 0 0 16px 16px; padding: 0; }
  .lb-story-media { display: none; }
  .lb-story-beats,
  .lb-story-scroll--single .lb-story-beats,
  .lb-story-scroll--photos .lb-story-beats { padding: 24px 20px 32px; gap: 34px; text-align: left; max-width: none; }
  .lb-story-beat { min-height: 0; display: block; opacity: 1 !important; transform: none !important; }
  .lb-story-sub,
  .lb-story-scroll--single .lb-story-sub { margin: 0; max-width: none; }
  .lb-story-inline {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: 14px;
    margin-top: 14px;
    background: #0f0f0f;
  }
  .lb-story-gallery {
    grid-template-columns: 1fr;
    padding: 20px;
    gap: 16px;
  }
}

/* ════════════════════════════════════════════════════════════════
   STORY-SCROLL — SEMENTARA hardcode ke 5 section demo (persis
   demo.tsx yang dikasih user), buat verifikasi dulu apakah mekanisme
   pin + rotate-in GSAP ScrollTrigger-nya jalan, sebelum disambung
   lagi ke data Works/Activities asli. Layout 1 kolom penuh (flex
   column, justify-content:space-between) — niru persis FlowSection's
   Tailwind classes (`flex flex-col justify-between gap-6 px-[4vw]
   pt-[clamp(2rem,8vw,4vw)] pb-[4vw]`), BUKAN grid 2 kolom lagi.
   Section dibuat & pin/rotate-nya diatur oleh
   works-activities-story.js (lihat setupScroll()).
   ════════════════════════════════════════════════════════════════ */
/* ── WORKS/ACTIVITIES split-screen paged navigation ──
   Diadaptasi dari konsep "ScrollAdventure" (lihat works-splitscroll.js):
   satu .split-page per Work/Activity, tiap page punya dua paruh yang
   posisinya absolute (kiri 0–50%, kanan 50–100%), paruh yang tidak
   aktif digeser transform ke luar layar (kiri turun, kanan naik) alih-
   alih pin/rotate scroll-linked seperti versi sebelumnya. Satu wheel/
   keydown/swipe event = satu halaman (lihat handleWheel/handleKeyDown
   di JS), bukan lagi native scroll — makanya .split-scroll di sini
   overflow:hidden, bukan overflow-y:auto seperti .story-scroll versi
   lama.
   Foto vs teks berselang-seling sisi per index (lihat buildPage() di
   JS) — satu paruh SELALU foto (atau placeholder gradient kalau belum
   ada), paruh lainnya SELALU teks (eyebrow + judul + deskripsi + stack
   tags untuk Works + meta), tidak pernah keduanya di paruh yang sama.
   Klik foto membuka #act-lightbox yang sudah ada (window.openActLightbox
   + window._actData) — sama seperti kartu masonry/fan-carousel lama. */
.split-scroll {
  position: relative;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
.split-track { position: relative; width: 100%; height: 100%; }

.split-page {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.split-half {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  overflow: hidden;
  will-change: transform;
}
.split-half--left  { left: 0; }
.split-half--right { left: 50%; }

/* Wrapper untuk gambar utama — flex dinamis: foto utuh (tidak pernah
   dipotong) dan tanpa bar kosong karena ukurannya dihitung dari rasio
   asli foto (fitCoverImage di works-splitscroll.js). Daerah yang tidak
   dipakai foto memakai latar halaman, bukan bar gelap buatan. */
.split-image-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: clamp(16px, 2.5vw, 36px);
}

.split-image {
  position: relative;
  display: block;
  width: fit-content;
  height: fit-content;
  max-width: 100%;
  max-height: 100%;
  margin: 0; 
  padding: 0; 
  border: none;
  background: transparent;
  cursor: pointer;
  z-index: 1;
  line-height: 0;
}
.split-photo {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 20px;
  overflow: hidden;
}
@media (hover: hover) and (pointer: fine) {
  .split-image:hover { filter: brightness(1.04); }
}
.split-image:focus-visible { outline: 2px solid var(--blue); outline-offset: -4px; }
.split-image--placeholder {
  cursor: default;
  background: linear-gradient(135deg, #221f1c, #121110);
}

.split-text {
  width: 100%; height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.25rem;
  padding: clamp(2rem, 6vw, 4rem);
  box-sizing: border-box;
}
/* Naikkan konten kedua paruh (kiri & kanan) 32px dari posisi center
   normal — permintaan khusus utk #works split-scroll. PAKAI PADDING,
   BUKAN transform: .split-text juga yang bawa warna background-nya
   (di-set inline lewat JS), jadi kalau kotaknya sendiri yang
   di-translate, bagian bawah kotak itu jadi kosong/tembus ke warna
   section di belakang (overflow:hidden di parent .split-half) — itu
   yang bikin kelihatan "warnanya kepotong". Nambah padding-bottom
   ekstra menggeser posisi teks ke atas tanpa memindahkan kotak
   warnanya sama sekali, jadi background tetap penuh 100% tinggi paruh. */
#works .split-text {
  padding-bottom: calc(clamp(2rem, 6vw, 4rem) + 64px);
}

.flow-eyebrow {
  display: flex; align-items: center; gap: 12px;
  font-size: 12px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; margin: 0; opacity: .8;
}
.flow-eyebrow::before {
  content: ''; width: 28px; height: 1px; background: currentColor; opacity: .55;
}
.flow-title {
  font-size: clamp(1.25rem, 2vw, 1.9rem);
  font-weight: 700; line-height: 1.15; text-transform: uppercase; letter-spacing: .01em;
  margin: 0;
  overflow-wrap: break-word;
  text-wrap: balance;
}
.flow-p {
  font-size: clamp(0.9rem, 1.25vw, 1.1rem);
  font-weight: 400; line-height: 1.6; max-width: 50ch; margin: 0;
  text-wrap: pretty;
}

.flow-badge {
  position: absolute; left: 14px; bottom: 14px; z-index: 2;
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface-1); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(200, 220, 255, 0.5); border-radius: 10px;
  padding: 6px 12px; font-size: 11px; font-weight: 700; color: var(--text-primary);
  box-shadow: 0 4px 16px rgba(0,0,0,.06); white-space: nowrap;
}
.flow-badge-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex-shrink: 0; }

.flow-stack { display: flex; flex-wrap: wrap; gap: 8px; }
/* pill tag adaptif terhadap palet tiap halaman (ada bg terang & gelap)
   supaya konsisten dan tidak bentrok dengan warna teks setempat */
.flow-stack .lb-stack-tag {
  color: currentColor;
  background: color-mix(in srgb, currentColor 14%, transparent);
  border-color: color-mix(in srgb, currentColor 30%, transparent);
}
.flow-meta {
  display: flex; align-items: center; gap: 8px;
  font-size: 12px; font-weight: 600; opacity: .65; letter-spacing: .12em; text-transform: uppercase; margin: 0;
}
.flow-meta::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: .6; }

/* Dot pagination — bulat kecil di tepi kanan, klik untuk lompat
   langsung ke halaman itu (di luar wheel/keydown/swipe). */
.split-dots {
  position: absolute;
  right: clamp(14px, 2.4vw, 28px);
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.split-dot {
  width: 8px; height: 8px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.6);
  background: rgba(255,255,255,.25);
  padding: 0; cursor: pointer;
  transition: transform .3s ease, background .3s ease;
}
.split-dot.is-active { background: #fff; transform: scale(1.3); }
.split-dot:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .split-half { transition: none !important; }
}

@media (max-width: 640px) {
  /* Mobile: tidak ada wheel-jack — halaman ditumpuk normal & discroll
     seperti dokumen biasa (persis pola fallback #works lama), foto di
     atas teks. */
  .split-scroll { overflow: visible; flex: none; }
  .split-track { height: auto; }
  .split-page {
    position: static;
    display: flex;
    flex-direction: column;
    min-height: auto;
    margin-bottom: 20px;
  }
  .split-half {
    position: static;
    width: 100%;
    height: auto;
  }
  .split-half--left  { order: 2; }
  .split-half--right { order: 2; }
  .split-half:has(.split-image-wrapper) { order: 1; }
  .split-half:has(.split-image) { order: 1; }
  .split-image-wrapper { min-height: 260px; }
  .split-image { min-height: 260px; }
  .split-text { padding: 20px; gap: 12px; }
  #works .split-text { padding-bottom: 20px; }
  .flow-title { font-size: clamp(1.2rem, 6.2vw, 1.8rem); }
  .split-dots { display: none; }
}

.lb-slideshow { width: 100%; height: 100%; position: relative; display: block; background: #0f172a; }
.lb-slide { width: 100%; height: 100%; object-fit: cover; display: none; position: absolute; inset: 0; margin: 0 auto; }
.lb-slide.active { display: block; animation: fadeIn 0.4s var(--ease-soft); }
.lb-arr { position: absolute; top: 50%; transform: translateY(-50%); background: var(--surface-1); border: none; width: 40px; height: 40px; border-radius: 50%; font-size: 24px; cursor: pointer; backdrop-filter: blur(4px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); color: var(--text-primary); display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; z-index: 10; }
.lb-arr:hover { background: var(--surface); transform: translateY(-50%) scale(1.1); }
.lb-prev { left: 16px; }
.lb-next { right: 16px; }
.lb-dots { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 6px; z-index: 10; }
.lb-dot { width: 8px; height: 8px; border-radius: 50%; border: none; background: var(--surface-1); cursor: pointer; transition: 0.3s ease; padding: 0; }
.lb-dot.active { background: #fff; transform: scale(1.3); }
.lb-ss-counter { position: absolute; top: 16px; left: 16px; background: rgba(0,0,0,0.6); color: #fff; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 99px; backdrop-filter: blur(4px); z-index: 10; }


/* ─── SINGLE PAGE TAB NAVIGATION ────────────────── */

/* Setiap section adalah halaman penuh tersembunyi */
body > section[id] {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  z-index: 2;
  /* #works (and every other full-page section here) is its own
     independent scroll container — body's `overscroll-behavior-y:
     contain` does NOT inherit down into a nested scroller. Without
     this, hitting the very top/bottom edge lets the browser
     rubber-band/chain scroll inside the section itself: the content
     is already fully scrolled, but the elastic bounce keeps emitting
     scroll deltas past that point. GSAP's ScrollTrigger (which drives
     #works's pin/rotate story-scroll) reacts to those phantom deltas
     and keeps re-toggling the pin right at the edge — that's the
     "sampai paling atas masih kayak bisa discroll, nyangkut2" feel.
     Containing overscroll here stops it at the source. */
  overscroll-behavior-y: contain;
  /* Hidden state */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Ruang untuk fixed nav: 24px margin-top + 72px height + 12px gap */
  padding-top: 108px;
  scrollbar-width: none;
  /* Scrollbar disembunyikan (dulu thin+biru) — scroll konten vertikal
     tetap jalan normal, cuma indikatornya yang dihilangkan supaya tidak
     ada garis biru menempel di tepi kanan layar. */
  /* Catatan: sengaja TIDAK ada `transition` di sini. Perpindahan
     opacity/transform/filter antar section sekarang sepenuhnya
     dikendalikan oleh satu master GSAP timeline di page-transitions.js
     (lihat file itu) — kalau CSS transition ikut menganimasikan
     property yang sama, browser akan mencoba meng-interpolasi di atas
     tween GSAP yang sudah berjalan, hasilnya animasi ganda/patah. Kalau
     JS gagal load, section tetap tampil/sembunyi instan lewat class ini
     (progressive enhancement, bukan cacat visual — cuma tidak animasi). */
}

body > section[id]::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* Active/visible state */
body > section[id].page-active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

/* Footer: sembunyikan di luar section, tampilkan dari dalam section home */
body > footer { display: none !important; }

/* ── Animasi konten saat section masuk ── */
@keyframes slideRightIn {
  0%   { opacity: 0; transform: translateX(40px); }
  100% { opacity: 1; transform: translate(0, 0); }
}
@keyframes slideLeftIn {
  0%   { opacity: 0; transform: translateX(-40px); }
  100% { opacity: 1; transform: translate(0, 0); }
}
@keyframes slideUpIn {
  0%   { opacity: 0; transform: translateY(24px); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOutFast {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

/* Sembunyikan elemen sebelum section aktif */
#works .page-header, #works .card,
#certificate .page-header, #certificate .pin-card,
#about .page-header, #about .fact, #about .connect-item {
  opacity: 0;
}

/* Animasi masuk saat section mendapat class page-active */
#home.page-active .hero-content,
#home.page-active .bento-grid,
#home.page-active .hero,
#home.page-active .hero-left,
#home.page-active .hero-right {
  animation: slideUpIn 0.6s cubic-bezier(0.17, 0.55, 0.55, 1) both;
}

#works.page-active .page-header, #works.page-active .card {
  animation: slideUpIn 0.7s cubic-bezier(0.17, 0.55, 0.55, 1) forwards;
}

#certificate.page-active .page-header,
#certificate.page-active .pin-card {
  animation: slideLeftIn 0.7s cubic-bezier(0.17, 0.55, 0.55, 1) forwards;
}

#about.page-active .page-header,
#about.page-active .fact,
#about.page-active .connect-item {
  animation: slideLeftIn 0.7s cubic-bezier(0.17, 0.55, 0.55, 1) forwards;
}

/* Stagger delay */
.page-active .page-header { animation-delay: 0.05s; }
.page-active .projects-grid .card:nth-child(1),
.page-active .pin-grid .pin-card:nth-child(1),
.page-active .facts-wrap .fact:nth-child(1) { animation-delay: 0.15s; }
.page-active .projects-grid .card:nth-child(2),
.page-active .pin-grid .pin-card:nth-child(2),
.page-active .facts-wrap .fact:nth-child(2) { animation-delay: 0.25s; }
.page-active .projects-grid .card:nth-child(3),
.page-active .pin-grid .pin-card:nth-child(3),
.page-active .facts-wrap .fact:nth-child(3) { animation-delay: 0.35s; }
.page-active .projects-grid .card:nth-child(4),
.page-active .pin-grid .pin-card:nth-child(4),
.page-active .facts-wrap .fact:nth-child(4) { animation-delay: 0.45s; }
.page-active .projects-grid .card:nth-child(n+5),
.page-active .pin-grid .pin-card:nth-child(n+5) { animation-delay: 0.55s; }



/* ═══════════════════════════════════════════════════════════
   NEUMORPHIC SOFT-UI OVERRIDE
   Menyeragamkan semua permukaan (navbar, card, modal, badge,
   lightbox, dll) ke gaya neumorphic #e8e8e8 dengan shadow ganda.
   ═══════════════════════════════════════════════════════════ */

/* Navbar */
nav {
  background: var(--neu-bg);
  border: none;
  box-shadow: var(--neu-shadow-sm);
}
nav:hover { box-shadow: var(--neu-shadow); transform: translateY(-2px); }

/* Filter / tab buttons */
.filter-btn {
  background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm);
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
.filter-btn:hover { background: var(--neu-bg); box-shadow: var(--neu-inset-sm); color: var(--dark); }
.filter-btn.active { box-shadow: var(--neu-inset-sm); }

/* Hero social icons */
.hero-socials a { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); }
.hero-socials a:hover { box-shadow: var(--neu-inset-sm); transform: none; }

/* Floating badges around hero photo */
.fbadge { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }
.fbadge:hover { box-shadow: var(--neu-shadow); }

/* Tech marquee strip */
.marquee-wrap { background: var(--neu-bg); border-top: none; }

/* About: intro photo card */
.intro-photo-wrap { background: #e0e0e0; border: none; box-shadow: var(--neu-shadow-lg); backdrop-filter: none; -webkit-backdrop-filter: none; }
.intro-photo-wrap:hover { box-shadow: var(--neu-shadow-lg); }

/* About: fact / connect items */
.fact { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }
.fact:hover { box-shadow: var(--neu-shadow); border-color: transparent; }

/* Let's Connect — form reads as a raised card with pressed-in fields,
   the classic neumorphic pairing, consistent with .fact above */
.connect-form { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); }
.cf-field input, .cf-field textarea { background: var(--neu-bg); border: none; box-shadow: var(--neu-inset-sm); }
.cf-field input:focus, .cf-field textarea:focus { box-shadow: var(--neu-inset), 0 0 0 3px var(--blue-lt); }
.cv-btn, .cf-submit { box-shadow: var(--neu-shadow-sm), 0 8px 20px rgba(37, 99, 235,.22); }
.cv-btn:hover, .cf-submit:hover { box-shadow: var(--neu-shadow), 0 12px 26px rgba(37, 99, 235,.3); }
.connect-item { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); }
.connect-item:hover { box-shadow: var(--neu-shadow); border-color: transparent; }
/* .connect-item juga dipakai sebagai hook animasi stagger pada judul & deskripsi
   (lihat #about.page-active .connect-item) — tanpa exclude ini, teks judul/paragraf
   ikut ter-boks jadi kartu terpisah, padahal secara konten satu kesatuan dengan
   .connect-info. Kembalikan ke teks polos, senada dengan hero di bagian atas. */
.connect-title.connect-item, .connect-desc.connect-item {
  display: block; background: none; box-shadow: none; border: none;
  padding: 0; border-radius: 0; gap: 0; transform: none;
}
.connect-title.connect-item:hover, .connect-desc.connect-item:hover {
  box-shadow: none; border-color: transparent; transform: none;
}

/* Works: project cards */
.card, .projects-grid .card {
  background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow);
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
.card:hover, .projects-grid .card:hover { box-shadow: var(--neu-shadow-lg); border-color: transparent; }
.card-thumb, .card-slideshow, .card-photo { background: #dcdcdc; }
.cs-arr { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }
.cs-arr:hover { background: var(--neu-bg); box-shadow: var(--neu-inset-sm); }
.cat-tag, .card-num { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }
.thumb-placeholder { background: #dcdcdc; }

/* Modal (works detail) */
.modal { background: var(--neu-bg); box-shadow: var(--neu-shadow-lg); }
.modal-slideshow { background: #dcdcdc; }
.modal-close { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }
.modal-close:hover { background: var(--dark); color: #fff; box-shadow: var(--neu-shadow-sm); }

/* Activity board (shared board surface) */
.board { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow); backdrop-filter: none; -webkit-backdrop-filter: none; }
.act-count, .cert-count { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); }

/* Certificate wall frames */
.cert-paper { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow); backdrop-filter: none; -webkit-backdrop-filter: none; }
.pin-card:hover .cert-paper { box-shadow: var(--neu-shadow-lg); }
.cert-paper::after { border-color: rgba(0,0,0,0.12); }
.frame-plate { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }

/* Lightbox (activities & certificate detail) */
.lightbox-inner { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-lg); backdrop-filter: none; -webkit-backdrop-filter: none; }
.lightbox-close { background: var(--neu-bg); color: var(--dark); border: none; box-shadow: var(--neu-shadow-sm); }
.lightbox-close:hover { background: #e11d48; color: #fff; }
.lb-body { background: var(--neu-bg); }
.lb-arr { background: var(--neu-bg); box-shadow: var(--neu-shadow-sm); backdrop-filter: none; -webkit-backdrop-filter: none; }
.lb-arr:hover { background: var(--neu-bg); box-shadow: var(--neu-inset-sm); }
.lb-dot { background: #bfbfbf; }
.lb-dot.active { background: var(--dark); }

/* Footer & stack row */
footer, .stack-row { background: var(--neu-bg); border-top: none; }

/* Skeleton loaders */
.skeleton-item, .skeleton-pin-card { background: var(--neu-bg); border: none; box-shadow: var(--neu-shadow-sm); }
/* ══════════════════════════════════════════════════════════════════
   PREMIUM MOTION LAYER — Apple / Vercel / Linear / Framer inspired
   Additive only: nothing above this line is modified. Everything here
   is opt-in via classes/ids added by premium.js, and gracefully does
   nothing if that script fails to load.
   ══════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  #cursor-spotlight, #bg-noise, .hero-glow, .fb-parallax { display: none !important; }
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

/* ── Cursor spotlight (canvas-based, see spotlight-cursor.js) ──
   Fixed full-viewport canvas, pointer-events disabled so it never
   blocks clicks/hover/input. The radial glow itself is painted by
   spotlight-cursor.js on every frame; this rule only positions the
   canvas and keeps it out of the interaction/scroll layer. ── */
#cursor-spotlight {
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 5;
  /* Multiply so the dark-gray glow reads as a soft darkening highlight
     on the site's white/light background instead of a flat gray disc. */
  mix-blend-mode: multiply;
}
@media (hover: none), (pointer: coarse) {
  #cursor-spotlight { display: none !important; }
}

/* ── film-grain noise (very subtle) ── */
#bg-noise {
  position: fixed; inset: 0; z-index: 6; pointer-events: none;
  opacity: 0.025;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMjAnIGhlaWdodD0nMTIwJz48ZmlsdGVyIGlkPSduJz48ZmVUdXJidWxlbmNlIHR5cGU9J2ZyYWN0YWxOb2lzZScgYmFzZUZyZXF1ZW5jeT0nMC45JyBudW1PY3RhdmVzPScyJyBzdGl0Y2hUaWxlcz0nc3RpdGNoJy8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScgZmlsdGVyPSd1cmwoI24pJy8+PC9zdmc+");
  background-size: 160px 160px;
}

@media (max-width: 768px) {
  #cursor-spotlight, #bg-noise { display: none; }
}

/* NOTE: the old custom cursor (#cursor-ring inverted blob + native
   cursor hidden via body.cursor-none) has been removed. The new
   cursor spotlight in spotlight-cursor.js is a pure ambient glow —
   it never hides or replaces the native browser cursor. ── */

/* ── Magnetic elements: GPU-friendly, JS drives x/y via inline transform ── */
.magnetic { will-change: transform; }

/* ── Navbar: scroll progress + glass reflection ── */
nav.nav-fix { overflow: hidden; }
nav.nav-fix::before {
  content: '';
  position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(180px 90px at var(--nav-mx, 50%) var(--nav-my, 0%),
    rgba(255,255,255,0.55), transparent 70%);
  opacity: 0; transition: opacity .4s ease;
}
nav.nav-fix:hover::before { opacity: 1; }
#nav-progress {
  position: absolute; left: 0; top: 0; height: 2px; width: 0%;
  background: linear-gradient(90deg, var(--blue), #8b5cf6);
  border-radius: 99px 0 0 0;
  transition: width .12s linear;
}

/* ── Hero: extra glow layer + parallax wrappers (added by JS/markup) ── */
.hero-glow {
  position: absolute; inset: -40px; z-index: 0; pointer-events: none;
  background: radial-gradient(220px 220px at 50% 40%, rgba(37, 99, 235,0.14), transparent 70%);
  filter: blur(6px);
  will-change: transform;
}
.fb-parallax { position: absolute; inset: 0; will-change: transform; }
.fb-parallax .fbadge { position: absolute; }
.fb-parallax.fbp1 { z-index: 4; }
.fb-parallax.fbp2 { z-index: 4; }
.fb-parallax.fbp3 { z-index: 4; }

.hero-photo-wrap, .hero-right { will-change: transform; }

/* Gradient sheen sweep on the highlighted heading words */
.hero-heading .hl {
  background-image: linear-gradient(100deg, #3b82f6 0%, #6366f1 45%, #3b82f6 100%);
  background-size: 220% 100%;
  animation: hlSheen 7s ease-in-out infinite;
}
@keyframes hlSheen {
  0%, 100% { background-position: 0% 0; }
  50%      { background-position: 100% 0; }
}

/* SplitText line masks — used only once JS has split the heading */
.split-line { overflow: hidden; display: block; }
.hero-heading.split-ready { opacity: 1; animation: none; }

/* ── Scroll-reveal (IntersectionObserver-driven, for within-section content) ── */
.fx-reveal {
  opacity: 0;
  transform: translateY(56px) scale(0.96);
  filter: blur(10px);
  will-change: transform, opacity, filter;
}
.fx-reveal.fx-in {
  opacity: 1;
  transform: translateY(0) scale(1);
  filter: blur(0px);
  transition: opacity .9s var(--ease-soft), transform .9s var(--ease-soft), filter .9s var(--ease-soft);
}

/* ── Project / cert / activity card premium hover polish ── */
.card, .pin-card, .msnry-card {
  position: relative;
  transform: translateZ(0);
}
.card::after {
  content: '';
  position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  padding: 1px;
  background: linear-gradient(135deg, rgba(59,130,246,0.55), rgba(139,92,246,0.35), transparent 60%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity .4s var(--ease-soft);
  z-index: 2;
}
.card:hover::after { opacity: 1; }

.card-thumb::before {
  content: '';
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(180deg, transparent 45%, rgba(15,23,42,0.35) 100%);
  opacity: 0;
  transition: opacity .45s var(--ease-soft);
}
.card:hover .card-thumb::before { opacity: 1; }

.card-stack { opacity: 0.72; transform: translateY(2px); transition: opacity .4s var(--ease-soft), transform .4s var(--ease-soft); }
.card:hover .card-stack { opacity: 1; transform: translateY(0); }

/* ── Tech-stack row hover (About / Works footer) ── */
.stack-item, .stack-row-item, .stack-tag {
  display: inline-flex; align-items: center;
  transition: color .3s var(--ease-soft), transform .3s var(--ease-soft), filter .3s var(--ease-soft);
}
.stack-item:hover, .stack-row-item:hover, .stack-tag:hover {
  transform: translateY(-3px) rotate(-2deg);
  filter: drop-shadow(0 4px 10px rgba(37, 99, 235,0.25));
}

/* ── Underline reveal micro-interaction for text links ── */
.card-link, .nav-links a span {
  position: relative;
}
.nav-links a span::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: -3px; height: 1px;
  background: currentColor; transition: right .35s var(--ease-soft);
}
.nav-links a:hover span::after { right: 0; }

/* ── Origin-expand handoff for modal/lightbox opening (see premium.js) ── */
.modal, .lightbox-inner { transform-origin: 50% 50%; }

/* GPU hint: this element's opacity/transform/filter are now animated
   entirely by the master timeline in page-transitions.js. */
body > section[id] { will-change: opacity, transform, filter; }

/* ── Navbar liquid indicator ── */
.nav-links { position: relative; }
#nav-liquid {
  position: absolute; top: 0; left: 0; z-index: 0;
  border-radius: 999px;
  background: var(--dark);
  opacity: 0;
  pointer-events: none;
  will-change: transform, width, height;
}
.nav-links a { position: relative; z-index: 1; }









@media (prefers-reduced-motion: reduce) {
  #nav-liquid {
    transition: none !important;
  }
}

/* ══════════════════════════════════════════════════════════════════
   CINEMATIC PAGE TRANSITION ENGINE — page-transitions.js
   ──────────────────────────────────────────────────────────────────
   Satu elemen "jembatan" visual: sebuah blob/glow yang tumbuh dari
   posisi menu yang diklik, sempat menutupi layar (di situ konten lama
   ditukar ke konten baru), lalu menyusut & memudar sambil layout baru
   selesai terbentuk. Warna & blur-nya sengaja memakai token yang sama
   dengan .hero-glow / spotlight lain di file ini, supaya terasa masih
   bagian dari identitas visual yang sama, bukan efek baru yang asing.
   ══════════════════════════════════════════════════════════════════ */

.transition-bridge {
  position: fixed;
  inset: 0;
  z-index: 45; /* di atas section (2), di bawah navbar (100) */
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
}

.bridge-shape {
  position: absolute;
  top: 0;
  left: 0;
  width: 440px;
  height: 440px;
  margin: -220px 0 0 -220px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 35% 35%,
      rgba(139, 92, 246, 0.30) 0%,
      rgba(37, 99, 235, 0.30) 40%,
      rgba(37, 99, 235, 0) 72%);
  filter: blur(38px);
  opacity: 0;
  transform: translate(0, 0) scale(0);
  will-change: transform, opacity, filter, border-radius;
}

@media (max-width: 768px) {
  .bridge-shape { width: 320px; height: 320px; margin: -160px 0 0 -160px; }
}

@media (prefers-reduced-motion: reduce) {
  .transition-bridge, .bridge-shape { display: none !important; }
}
/* ══════════════════════════════════════════════════════════════════
   PETAL NAV — GLASS ENHANCEMENT
   Purely additive layer on top of the existing .page-drag-hint /
   .drag-hint-petal / .drag-hint-btn system (see index.html + nav-glass.js).
   Nothing here removes or restructures the original curved black-corner
   navigation — it only adds depth, light, and interactivity to it.
   ══════════════════════════════════════════════════════════════════ */

:root {
  --petal-glow-x: 50%;
  --petal-glow-y: 50%;
  --petal-tilt: 0deg;
}

/* Glass gradient fill replaces the flat --dark fill (set by nav-glass.js
   onto the SVG path via a <linearGradient>/<radialGradient> it injects). */
.drag-hint-petal path {
  filter: drop-shadow(0 18px 40px rgba(8, 12, 24, .38));
}
.is-page-dragging .drag-hint-petal path {
  filter: url(#petalLiquidDistort) drop-shadow(0 18px 40px rgba(8, 12, 24, .45));
}

/* Cursor-following glow + soft reflection sitting above the petal fill.
   Rectangular clip is fine — it stays close to the curved edge visually
   because the wrap itself hugs the screen edge. */
.petal-glow-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity .35s var(--ease-soft, ease);
  background: radial-gradient(
    260px 260px at var(--petal-glow-x) var(--petal-glow-y),
    rgba(120, 170, 255, .55),
    rgba(120, 170, 255, .12) 45%,
    transparent 70%
  );
}
.page-drag-hint--right .petal-glow-layer { right: 0; }
.page-drag-hint.is-active .petal-glow-layer,
.page-drag-hint.is-hovering .petal-glow-layer { opacity: 1; }

/* ── Button: glass depth, reflections, breathing, hover, drag states ── */
.drag-hint-btn {
  pointer-events: auto;
  cursor: pointer;
  background:
    linear-gradient(155deg, rgba(255,255,255,.92), rgba(255,255,255,.62)) padding-box;
  backdrop-filter: blur(14px) saturate(160%);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  box-shadow:
    0 10px 26px rgba(15, 23, 42, .20),
    0 1px 0 rgba(255,255,255,.8) inset,
    0 -6px 14px rgba(15, 23, 42, .06) inset;
  transition: box-shadow .35s var(--ease-soft, ease), background .35s var(--ease-soft, ease), border-color .35s ease;
  will-change: transform;
}
.drag-hint-btn::before {
  /* dynamic specular sweep */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.55) 48%, transparent 62%);
  background-size: 220% 220%;
  background-position: 120% 0%;
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
}
.page-drag-hint.is-hovering .drag-hint-btn::before {
  opacity: 1;
  animation: petalSweep 1.1s ease forwards;
}
@keyframes petalSweep {
  from { background-position: 120% 0%; }
  to   { background-position: -20% 100%; }
}
.page-drag-hint.is-hovering .drag-hint-btn {
  border-color: rgba(37, 99, 235, .55);
  background:
    linear-gradient(155deg, rgba(219,234,255,.95), rgba(255,255,255,.7)) padding-box;
  box-shadow:
    0 14px 34px rgba(37, 99, 235, .28),
    0 1px 0 rgba(255,255,255,.9) inset,
    0 -6px 14px rgba(37, 99, 235,.10) inset;
  color: #1d4ed8;
}
.page-drag-hint.is-active .drag-hint-btn,
.page-drag-hint.is-hovering .drag-hint-btn {
  animation: none;
}

/* Idle breathing (paused the instant hover/drag/active takes over —
   handled by nav-glass.js toggling classes, GSAP drives the actual
   scale/glow tween so it can blend smoothly with hover/drag states). */
.page-drag-hint:not(.is-hovering):not(.is-active) .drag-hint-btn {
  animation: dragHintBreathe 4.6s ease-in-out infinite;
}
@keyframes dragHintBreathe {
  0%, 100% { box-shadow: 0 10px 26px rgba(15,23,42,.20), 0 1px 0 rgba(255,255,255,.8) inset, 0 -6px 14px rgba(15,23,42,.06) inset; }
  50%      { box-shadow: 0 14px 32px rgba(37, 99, 235,.22), 0 1px 0 rgba(255,255,255,.9) inset, 0 -6px 14px rgba(37, 99, 235,.08) inset; }
}

/* Ripple feedback on click */
.drag-hint-ripple {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(37, 99, 235,.45), rgba(37, 99, 235,0) 70%);
  transform: translate(-50%, -50%) scale(0);
  pointer-events: none;
  width: 12px; height: 12px;
  opacity: .9;
}

/* Full-screen "glass sheet" expand clone used for click-triggered /
   commit-finished transitions — injected + animated by nav-glass.js. */
.petal-expand-clone {
  position: fixed;
  top: 0; height: 100vh;
  z-index: 90;
  pointer-events: none;
  will-change: transform;
}
.petal-expand-clone path {
  filter: drop-shadow(0 30px 60px rgba(8,12,24,.5));
}

/* Ambient background softening while a page-drag is in progress, giving
   the "pulling a glass sheet off the page" depth cue. */
.page-drag-blur {
  position: fixed;
  inset: 0;
  z-index: 35;
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  transition: backdrop-filter .25s ease;
  pointer-events: none;
}
.is-page-dragging .page-drag-blur {
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

@media (hover: none), (pointer: coarse) {
  .page-drag-hint.is-hovering .drag-hint-btn::before { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .drag-hint-btn { animation: none !important; }
  .petal-expand-clone, .drag-hint-ripple { transition: none !important; animation: none !important; }
}
/* ══════════════════════════════════════════════════════════════
   CONSOLIDATED FROM <head> INLINE <style> BLOCKS (index.html)
   ══════════════════════════════════════════════════════════════
   These three blocks used to live directly in index.html's
   <head>, stacked after the style.css <link>. They were patched
   in one at a time (see the 'FIX:' comments below) which left
   the page's CSS split across two places with no single source
   of truth. Moved here verbatim, in original order, so cascade
   behavior is unchanged — they still load after every other rule
   above, exactly as before. Next actual edit to any of these
   selectors should also remove the now-unnecessary !important
   where the specificity fight it was solving no longer exists,
   but that's a separate, riskier pass. */

    /* Background: flat solid page color (voronoi/cell-network pattern removed). */
    html, body {
      background-color: var(--bg-page) !important;
      transition: background-color 0.4s var(--ease-soft, ease);
    }
    /* ══════════════════════════════════════════
       TYPOGRAPHY REDESIGN — clean & comfortable
       ══════════════════════════════════════════ */

    /* Base font switch */
    body, * {
      font-family: 'DM Sans', 'Plus Jakarta Sans', sans-serif !important;
    }

    /* ── HERO heading ── */
    .hero-heading {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
      font-size: clamp(32px, 4.2vw, 58px) !important;
      font-weight: 700 !important;
      line-height: 1.15 !important;
      letter-spacing: -0.02em !important;
      color: var(--text-primary) !important;
    }

    /* highlight words: subtle gradient instead of loud blue */
    .hero-heading .hl {
      background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%) !important;
      -webkit-background-clip: text !important;
      -webkit-text-fill-color: transparent !important;
      background-clip: text !important;
      font-weight: 800 !important;
    }

    /* ── PAGE titles (Selected Works, Certificate Board) ── */
    .page-title {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
      font-size: clamp(24px, 3vw, 40px) !important;
      font-weight: 700 !important;
      line-height: 1.15 !important;
      letter-spacing: -0.025em !important;
      color: var(--text-primary) !important;
      margin: 0 0 2px !important;
    }

    /* ── Works hero headings (Selected Works / Selected Activities) ──
       Same face + weight as .hero-heading on Home (Plus Jakarta Sans,
       700) instead of the 900 these carried before — 900 isn't one of
       the weights the Google Fonts <link> loads for Plus Jakarta Sans
       (only 400–800), so it was rendering as a browser-synthesized
       fake-bold that reads noticeably heavier/uneven next to the real
       700 on Home. This is the one fix that makes both headings feel
       like the same typeface again. */
    .works-hero-title {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
      font-weight: 700 !important;
    }

    /* ── Eyebrow labels (PORTFOLIO, CREDENTIALS, MOMENTS…) ── */
    .page-eyebrow {
      font-family: 'DM Sans', sans-serif !important;
      font-size: 10px !important;
      font-weight: 600 !important;
      letter-spacing: 0.16em !important;
      text-transform: uppercase !important;
      color: var(--primary) !important;
      margin-bottom: 6px !important;
      opacity: 0.85 !important;
    }

    /* ── About Me intro name ── */
    .intro-name {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
      font-size: clamp(36px, 5vw, 64px) !important;
      font-weight: 700 !important;
      line-height: 1.05 !important;
      letter-spacing: -0.03em !important;
      color: var(--text-primary) !important;
    }
    .intro-name .line2 {
      font-weight: 800 !important;
      background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%) !important;
      -webkit-background-clip: text !important;
      -webkit-text-fill-color: transparent !important;
      background-clip: text !important;
    }

    /* ── Navbar logo ── */
    .nav-logo {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
      font-weight: 700 !important;
      font-size: 14px !important;
      letter-spacing: -0.01em !important;
    }

    /* ── Section labels & mono text ── */
    .intro-mono, .sec-label, .board-label {
      font-family: 'JetBrains Mono', monospace !important;
      font-size: 11px !important;
      letter-spacing: 0.08em !important;
      color: var(--text-secondary) !important;
    }

    /* ── Card titles ── */
    .card-title {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
      font-weight: 600 !important;
      font-size: 15px !important;
      letter-spacing: -0.01em !important;
      line-height: 1.3 !important;
    }

    /* ── Bio text ── */
    .intro-bio {
      font-family: 'DM Sans', sans-serif !important;
      font-size: 15px !important;
      font-weight: 400 !important;
      line-height: 1.7 !important;
      color: var(--text-secondary) !important;
    }

    /* Keep terminal/code in mono */
    .terminal-box, .terminal-body, .t-prompt, .t-text {
      font-family: 'JetBrains Mono', monospace !important;
    }

    /* ── FIX: force social media icons to render ── */
    .connect-item svg {
      display: inline-block !important;
      flex-shrink: 0 !important;
      width: 18px !important;
      height: 18px !important;
      fill: currentColor !important;
      color: var(--text-primary) !important;
      opacity: 1 !important;
      visibility: visible !important;
    }
    /* ── FIX: works harus muat 1 halaman penuh ── */

    /* ── WORKS: plain flowing layout ──
       Projects and Activities used to be two exclusive tab panels
       (.works-view + .is-active display toggle), then later a pinning/
       rotating "story scroll" (FlowArt) between the two. Both are
       gone — this is now just one flex column (.works-views) that
       renders both chapters in document order and scrolls normally;
       #works itself is the scroll container (overflow-y:auto, see the
       #works rule further down). */
    .works-views {
      display: flex;
      flex-direction: column;
      position: relative;
    }

    /* ── WORKS → ACTIVITIES wheel-jacked cinematic slider ──
       Projects and Activities are stacked as two absolutely-positioned
       full-size panels inside .works-views (which becomes their
       positioning context + gets its own height so inset:0 has
       something to fill). Only one panel is interactive/visible at a
       time; the actual fade+slide transition between them is a GSAP
       timeline driven by a wheel listener — see the cinematic-slider
       module script — not a CSS transition, since the scroll input
       itself needs to be "swallowed" for the animation's duration
       (that's the "jack" part) rather than just crossfading passively
       as you scroll. */
    .works-views {
      position: relative;
      flex: 1 1 auto;
      min-height: 0;
      overflow: hidden;
    }
    .works-projects,
    .works-activities {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      overflow-y: auto;
      overflow-x: hidden;
      -webkit-overflow-scrolling: touch;
      will-change: opacity, transform;
      scrollbar-width: none;
      -ms-overflow-style: none;
    }
    .works-projects::-webkit-scrollbar,
    .works-activities::-webkit-scrollbar {
      display: none;
      width: 0;
      height: 0;
    }
    /* Panel that's currently off-stage shouldn't intercept clicks or
       scroll while it sits invisible (opacity:0, set by the slider's
       GSAP timeline) behind the active one. Toggled by the slider JS. */
    .works-page-inert {
      pointer-events: none;
    }

    /* Chapter-break hairline doesn't fit a snap+crossfade transition —
       there's no continuous scroll to "break" mid-way through anymore. */
    .works-transition { display: none !important; }

    /* ── Projects — Coverflow Carousel chapter. Full-bleed blue panel
       (edge-to-edge, no card margin/radius) so its color reads as the
       page's actual background, not an inset card floating on a
       lighter page — see #works's own background-color below, which
       is kept in sync with whichever panel is active so even the
       nav-clearance strip behind the navbar matches. ── */
    .works-projects {
      display: flex;
      flex-direction: column;
      background: #E4F0F6;
      /* Extra space on top of the usual breathing room so the sheet's
         own background still bleeds to y:0 behind the floating nav
         (nav sits at top:24px, height:72px, so clearance needs to stay
         at least ~108px), while .works-projects-layout (flex:1,
         anchored to the top — see below) only starts filling space
         after this padding, so the carousel clears the navbar instead
         of sitting under it. The extra breathing room on top of that
         108px floor is trimmed down a bit — combined with the layout's
         own flex-start alignment below, the old value left too much
         empty space under the caption on shorter/wider viewports. */
      padding: calc(108px + clamp(16px, 2.5vw, 32px)) 0 clamp(28px, 4vw, 56px);
      font-family: 'Plus Jakarta Sans', sans-serif !important;
    }

    /* ── Chapter-break divider between Projects and Activities — a
       single hairline, not a divider bar or a second nav. Grows in
       via the site's usual .fx-reveal/.fx-in scroll-reveal pattern
       (see setupScrollReveal() in premium.js), so it earns its reveal
       through scroll instead of sitting pre-drawn. Generous vertical
       padding gives both chapters real breathing room instead of a
       hard cut. ── */
    .works-transition {
      flex-shrink: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 56px 0 40px;
    }
    .works-transition-line {
      width: 72px;
      height: 1px;
      background: linear-gradient(90deg, transparent, var(--glass-border-hi, #cbd5e1), transparent);
    }
    /* Overrides the generic .fx-reveal transform so the line reads as
       "drawing itself in" (width) rather than the uniform zoom used
       elsewhere — a small, purposeful departure from a plain fade. */
    .works-transition-line.fx-reveal { transform: scaleX(0.3); filter: blur(3px); }
    .works-transition-line.fx-reveal.fx-in { transform: scaleX(1); filter: blur(0); }

    /* ── Activities — secondary chapter. Fills its absolutely-positioned
       panel exactly (height:100% from the .works-projects/.works-activities
       rule above) regardless of how few activities there are, and scrolls
       internally (overflow-y:auto, same rule) once the masonry grid grows
       taller than one screen — the wheel-jack only owns the Projects↔
       Activities transition itself, not scrolling within Activities. ── */
    .works-activities {
      display: flex;
      flex-direction: column;
      height: 100%;
      background: #0A0F1E;
      color: #fff;
      font-family: 'Plus Jakarta Sans', sans-serif !important;
    }

    /* ── Activity count — light-on-dark variant of the default
       .act-count chip (which assumes the light page background) now
       that Activities sits on its own dark "sheet". Same shape,
       just inverted for contrast. White text/borders read fine on
       both the old blue and this dark background, so no further
       color changes needed here after the panel swap. ── */
    .works-activities .act-count {
      color: rgba(255,255,255,.75);
      border-color: rgba(255,255,255,.25);
      background: rgba(255,255,255,.08);
    }
    .works-activities .act-count span { color: #fff; }

    /* ── masonry container ──
       Top padding moved here from the now-hidden .works-subhead, so
       the grid still clears the floating navbar even without that
       row taking up space. ── */
    .masonry-wrap {
      padding: 28px 5vw 40px;
      flex: 1;
      overflow-x: hidden;
    }

    /* ── Shuffle button — floating icon button, top-right of the
       Activities panel, matching the reference gallery's refresh
       control. Reshuffles the masonry order on click (see
       setupMasonryShuffle() in the activities module script). ── */
    .msnry-shuffle-btn {
      position: absolute;
      /* 108px (nav-clearance, now that the Activities sheet is
         full-bleed to y:0 — see #works/.works-activities-hero) + the
         original 24px offset, so it lands in the same visual spot
         relative to the grid as before, just clear of the floating
         nav instead of sitting underneath it. */
      top: 132px;
      right: 5vw;
      z-index: 20;
      width: 40px;
      height: 40px;
      display: flex;
      align-items: center;
      justify-content: center;
      border: none;
      border-radius: 12px;
      background: rgba(255,255,255,.10);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      color: #fff;
      cursor: pointer;
      transition: background .2s var(--ease-soft, ease), transform .2s var(--ease-soft, ease);
    }
    .msnry-shuffle-btn:hover { background: rgba(255,255,255,.18); }
    .msnry-shuffle-btn:active { transform: scale(0.92); }
    .msnry-shuffle-btn svg.is-spinning { animation: msnryShuffleSpin .5s ease; }
    @keyframes msnryShuffleSpin { from { transform: rotate(0deg); } to { transform: rotate(180deg); } }

    /* ── ACTIVITY: masonry board — adapted from React Bits' <Masonry/>
       component (bin-packed columns, GSAP blur-to-focus entrance,
       per-card hover scale) into this project's vanilla JS/CSS stack
       — no React/build-step here, so layout math + animation are done
       in plain JS (see the module script) instead of hooks/GSAP-React.
       This just provides the positioning context; every card is
       absolutely placed by JS with an explicit left/top/width/height
       (animated via GSAP x/y/width/height), same technique the
       original component uses. Pure image tiles now — no overlay
       text/tags, matching the reference's clean look. ── */
    .masonry-grid {
      position: relative;
      width: 100%;
      padding-top: 4px;
      overflow-x: hidden;
    }
    .msnry-item {
      position: absolute;
      top: 0; left: 0;
      padding: 4px;
      will-change: transform, width, height, opacity, filter;
    }
    .msnry-card {
      position: relative;
      display: block;
      width: 100%;
      height: 100%;
      border-radius: 14px;
      overflow: hidden;
      cursor: pointer;
      background: #0A0F1E;
      box-shadow: 0 10px 28px -14px rgba(0,0,0,.5), 0 3px 8px -4px rgba(0,0,0,.3);
      will-change: transform;
    }
    .msnry-card img {
      position: absolute; inset: 0;
      width: 100%; height: 100%;
      object-fit: cover; object-position: 50% 30%;
      display: block;
      filter: grayscale(100%);
      transition: filter .35s var(--ease-soft, ease);
    }
    @media (hover: hover) and (pointer: fine) {
      .msnry-card:hover img { filter: grayscale(0%); }
    }
    /* no-photo fallback — plain category-tinted gradient, no label
       text, so it stays consistent with the clean, tag-free cards */
    .msnry-placeholder {
      position: absolute; inset: 0;
    }

    /* category-tinted placeholder gradients (kept for cards without a
       photo yet — cat-coded like the rest of the site, but no label
       text on top, per the clean/tag-free card design) */
    .mbg-talk     { background:linear-gradient(135deg,#0d1b3e,#162952); }
    .mbg-compete  { background:linear-gradient(135deg,#1a0505,#3b0f0f); }
    .mbg-collab   { background:linear-gradient(135deg,#021a10,#063020); }
    .mbg-training { background:linear-gradient(135deg,#1a0d00,#2a1500); }
    .mbg-project  { background:linear-gradient(135deg,#1a0010,#2d0020); }
    .mbg-org      { background:linear-gradient(135deg,#0e0520,#1e0a40); }

    /* ── WORKS: plain scrollable page panel ──
       #works is just a normal scroll container: Projects (its own
       dark coverflow-carousel panel, see .works-projects) sits at the
       top, Activities follows directly below it, and the reader moves
       between them with ordinary scrolling — no pin/rotate "story
       scroll" mechanic anymore. Background starts matching Projects
       (blue) rather than the site's normal light page color — Projects
       and Activities are both full-bleed colored panels now (no card
       margin), so #works's own background is what shows through the
       nav-clearance strip up top (behind the floating navbar) and
       needs to actually match instead of leaving a mismatched light
       gap there. On desktop this is kept in sync with the active panel
       by the wheel-jack slider itself (a GSAP tween on this same
       background-color, timed with the panel flip — see
       setupWorksPageFlipSlider() in the activities module script); on
       mobile there's no jacking, so it just stays blue, matching
       Projects at the top of the plain stacked flow. ── */
    #works {
      height: 100vh;
      max-height: 100vh;
      display: flex;
      flex-direction: column;
      box-sizing: border-box;
      overflow-y: auto;
      overflow-x: hidden;
      scroll-behavior: auto;
      -webkit-overflow-scrolling: touch;
      background: #E4F0F6;
      /* Desktop only (see max-width:640px override further down, which
         restores the standard 108px so mobile's plain static-stacked
         flow still clears the floating nav normally): the wheel-jack
         sheets (.works-projects/.works-activities) are absolutely
         positioned inset:0 of .works-views, and an absolutely
         positioned element's inset is measured from its containing
         block's PADDING box — so as long as #works itself reserved
         108px of top padding, the sheets could never actually reach
         y:0, no matter how well their background-color was kept in
         sync with that gap. Zeroing it here lets the sheets truly
         cover the full 100vh, edge to edge, behind the floating nav —
         nav-clearance for the actual readable content (heading, cards,
         grid) is now added as internal padding on that content itself,
         see .works-projects / .works-activities-hero below. */
      padding-top: 0;
    }
    @media (prefers-reduced-motion: reduce) {
      #works { scroll-behavior: auto; }
    }
    #works .page-header  { flex-shrink: 0 !important; padding-top: 10px !important; padding-bottom: 6px !important; }
    #works .page-title   { font-size: clamp(22px, 3.5vw, 38px) !important; margin: 0 !important; }
    #works hr.divider    { margin: 6px 0 !important; flex-shrink: 0 !important; }

    /* ── FIX: popup/modal ketiban navbar ──
       Section (#works, #certificate, dst) punya stacking context
       sendiri (position:fixed + transform) dengan z-index:2, jauh di bawah
       navbar (z-index:100). Jadi popup di dalamnya—walau z-index tinggi—
       tetap terjepit di bawah navbar. Begitu ada popup yang lagi terbuka
       di dalam section itu, angkat z-index section-nya di atas navbar.
       (.lightbox/#act-lightbox is exempt now — it lives directly under
       <body>, not nested in any section, so it stacks purely on its own
       z-index:99999 and doesn't need this hack.) */
    body > section[id]:has(.modal-overlay.open),
    body > section[id]:has(.fan-detail-overlay.open) {
      z-index: 1001 !important;
    }
    /* fan-wrap fills whatever height .works-projects gives it (its
       min-height, not a flex-fill of a locked-height #works anymore)
       and stays vertically centered within that hero chapter — full
       rule is with the rest of the card-stack styling further down. */

    /* ── WORKS: filter chips ── */
    .works-filters {
      display: flex; gap: 8px; flex-wrap: wrap;
      padding: 2px 0 14px;
    }
    .wf-chip {
      font-family: 'Plus Jakarta Sans', sans-serif;
      font-size: 12.5px; font-weight: 600;
      padding: 8px 16px; border-radius: 99px;
      border: 1.5px solid #e2e6ee; background: #fff;
      color: #64748b; cursor: pointer;
      display: inline-flex; align-items: center; gap: 7px;
      transition: all .25s cubic-bezier(.22,1,.36,1);
    }
    .wf-chip .wf-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .5; }
    .wf-chip:hover { border-color: #cfd6e4; color: #0f172a; }
    .wf-chip.active { background: #0f172a; border-color: #0f172a; color: #fff; }
    .wf-chip.active .wf-dot { background: #fff; opacity: 1; }
    .wf-chip.active[data-cat="iot"]    { background: #2563eb; border-color: #2563eb; }
    .wf-chip.active[data-cat="ml"]     { background: #7c3aed; border-color: #7c3aed; }
    .wf-chip.active[data-cat="cv"]     { background: #059669; border-color: #059669; }
    .wf-chip.active[data-cat="web"]    { background: #d97706; border-color: #d97706; }
    .wf-chip.active[data-cat="design"] { background: #db2777; border-color: #db2777; }
    .bento-item.wf-hidden { display: none !important; }

    /* ── WORKS MORPH GALLERY (vanilla JS + GSAP virtual-scroll) ──────
       Menggantikan bento-grid/MasonryEngine. Konsepnya diadaptasi dari
       komponen React "scroll-morph-hero" (scatter → line → circle →
       bottom-arc, flip-card, parallax cursor) ke stack asli proyek ini
       (vanilla JS + GSAP), lihat works-morph.js. Tidak ada React/
       framer-motion di sini — animasi ditulis manual lewat morphValue
       yang di-tween GSAP lalu diterapkan tiap frame di JS. ── */

    .morph-stage {
      position: relative;
      width: 100%;
      overflow: hidden;
      cursor: grab;
    }
    .morph-stage.is-grabbing { cursor: grabbing; }

    .morph-intro {
      position: absolute;
      left: 50%; top: 46%;
      transform: translate(-50%, -50%);
      text-align: center;
      z-index: 2;
      pointer-events: none;
      width: min(560px, 82vw);
    }
    .morph-intro-title {
      font-family: 'Plus Jakarta Sans', sans-serif;
      font-size: clamp(18px, 2.6vw, 26px);
      font-weight: 700;
      color: #0f172a;
      margin: 0 0 8px;
      letter-spacing: -0.01em;
    }
    .morph-intro-sub {
      font-family: 'JetBrains Mono', monospace;
      font-size: 10.5px;
      font-weight: 700;
      letter-spacing: .22em;
      color: #94a3b8;
      margin: 0;
    }

    .morph-focus {
      position: absolute;
      left: 50%; top: 6%;
      transform: translateX(-50%);
      text-align: center;
      z-index: 3;
      pointer-events: none;
      width: min(620px, 86vw);
      opacity: 0;
    }
    .morph-focus-cat {
      font-family: 'JetBrains Mono', monospace;
      font-size: 10px; font-weight: 700; letter-spacing: .16em;
      color: var(--focus-color, #2563eb);
      text-transform: uppercase;
      margin: 0 0 6px;
    }
    .morph-focus-title {
      font-family: 'Plus Jakarta Sans', sans-serif;
      font-size: clamp(18px, 2.6vw, 28px);
      font-weight: 700; color: #0f172a;
      margin: 0 0 6px; letter-spacing: -0.01em;
    }
    .morph-focus-desc {
      font-size: 12.5px; color: #64748b;
      margin: 0; line-height: 1.5;
      max-width: 480px; margin: 0 auto;
    }

    .morph-field {
      position: absolute;
      inset: 0;
      z-index: 1;
    }

    .morph-hint {
      position: absolute;
      left: 50%; bottom: 14px;
      transform: translateX(-50%);
      display: flex; gap: 5px;
      z-index: 3;
      pointer-events: none;
      opacity: 0;
      transition: opacity .4s ease;
    }
    .morph-hint.show { opacity: 1; }
    .morph-hint span {
      width: 5px; height: 5px; border-radius: 50%;
      background: #cbd5e1;
      transition: background .25s ease, transform .25s ease;
    }
    .morph-hint span.active { background: #2563eb; transform: scale(1.4); }

    /* ── card: posisi/ukuran/rotasi/scale murni dikendalikan JS lewat
       transform (bukan top/left), supaya interpolasi 60fps mulus ── */
    .card {
      position: absolute;
      top: 0; left: 0;
      will-change: transform, opacity;
      cursor: pointer;
      perspective: 900px;
    }
    .card-inner {
      position: relative;
      width: 100%; height: 100%;
      transform-style: preserve-3d;
      transition: transform .55s cubic-bezier(.22,1,.36,1);
    }
    .card:hover .card-inner,
    .card.is-flipped .card-inner { transform: rotateY(180deg); }

    .card-face {
      position: absolute; inset: 0;
      backface-visibility: hidden;
      -webkit-backface-visibility: hidden;
      border-radius: 12px;
      overflow: hidden;
      box-shadow: 0 10px 30px -12px rgba(15,23,42,.28);
    }
    .card-front {
      background-size: cover;
      background-position: center;
      background-color: #eef1f6;
    }
    .card-front::after {
      content: '';
      position: absolute; inset: 0;
      background: linear-gradient(to top, rgba(8,12,24,.55) 0%, transparent 45%);
    }
    .card-back {
      transform: rotateY(180deg);
      background: #0b1220;
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      text-align: center; padding: 10%;
      border: 1px solid rgba(255,255,255,.08);
    }
    .card-back-label {
      font-family: 'JetBrains Mono', monospace;
      font-size: 9px; font-weight: 700; letter-spacing: .14em;
      color: var(--card-color, #2563eb);
      text-transform: uppercase;
      margin: 0 0 4px;
    }
    .card-back-title {
      font-family: 'Plus Jakarta Sans', sans-serif;
      font-size: 11px; font-weight: 700; color: #fff;
      line-height: 1.3;
      display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
      overflow: hidden;
    }
    .card .cat-badge {
      position: absolute; top: 7px; left: 7px; z-index: 2;
      width: 22px; height: 22px;
      border-radius: 7px;
      display: flex; align-items: center; justify-content: center;
      background: rgba(255,255,255,.92);
      color: var(--card-color, #2563eb);
      box-shadow: 0 2px 6px rgba(15,23,42,.14);
    }
    .card .cat-badge svg { width: 12px; height: 12px; }
    .card-title-strip {
      position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
      padding: 8% 8% 6%;
      pointer-events: none;
    }
    .card-title {
      font-family: 'Plus Jakarta Sans', sans-serif;
      font-weight: 700; color: #fff;
      font-size: clamp(9px, 1vw, 12px);
      line-height: 1.25;
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
      overflow: hidden;
      text-shadow: 0 1px 4px rgba(0,0,0,.35);
    }

    .card.is-focused .card-face {
      box-shadow: 0 18px 46px -14px rgba(15,23,42,.4);
    }

    @media (prefers-reduced-motion: reduce) {
      .card-inner { transition: none; }
    }

    #works .morph-field { position: relative; z-index: 1; }

    .morph-field.is-skeleton {
      display: flex; align-items: center; justify-content: center; gap: 10px;
    }
    .card-skel-dot {
      width: 14px; height: 14px; border-radius: 50%;
      background: #dbe3ee;
      animation: skelPulse 1.1s ease-in-out infinite;
    }
    @keyframes skelPulse {
      0%, 100% { opacity: .35; transform: scale(.85); }
      50%      { opacity: 1;  transform: scale(1.15); }
    }

    /* ══════════════════════════════════════════════════════════
       WORKS COVERFLOW CAROUSEL — ported from a shadcn/React
       <CoverflowCarousel /> reference component into vanilla JS.
       Cards fan out symmetrically on both sides of the focused centre
       card (classic Apple Coverflow: tilt + recede + fade with
       distance from centre), replacing the earlier one-directional
       "depth rail" fan. Sits on its own dark Projects panel
       (.works-projects). Clicking the focused/centre card opens the
       existing full-detail overlay (gallery, description, external
       link); clicking any other visible card brings it into focus.
       See works-fan-carousel.js for the tilt/drag/wheel/autoplay
       logic (class names kept as .depth-carousel* to avoid touching
       every DOM hook — only the transform math inside changed). ══ */

    /* ── PROJECTS layout — vertical stack: the fullscreen coverflow
       carousel is the focal point at the top, the project readout
       (title/category/metadata) sits centered directly beneath it,
       matching the reference's structure. Replaces the old two-column
       info-left/carousel-right row. ── */
    #works .works-projects-layout {
      position: relative;
      z-index: 1;
      flex: 1 1 0;
      min-height: 0;
      width: 100%;
      margin: 0 auto;
      /* Was justify-content:center, which centers the carousel+caption
         block in whatever vertical space is left after the navbar
         clearance above — on shorter/wider viewports that leaves a much
         bigger gap below the caption than above the carousel (the
         caption block is short, so centering it pushes everything down
         into the middle of a tall empty area). flex-start + a small,
         deliberate top gap pulls the whole block up so it reads as
         anchored just under the navbar, not floating mid-page. */
      padding: clamp(4px, 2.5vh, 28px) clamp(12px, 2vw, 32px) 0;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
      /* Final polish pass: was clamp(20px, 3vw, 36px) — the carousel
         and the project readout beneath it felt like two separate
         blocks rather than one continuous unit. Pulled in so the
         eyebrow/title reads as a direct continuation of the active
         card, while still leaving clear breathing room (not flush). */
      gap: clamp(12px, 2vw, 22px);
      overflow: visible;
    }

    #works .fan-wrap {
      position: relative;
      width: 100%;
      flex: 0 0 auto;
      min-height: 0;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: center;
      overflow: visible;
    }

    /* ── EDGE FADE — cards nearing the left/right edge of the carousel
       dissolve into the panel background instead of hard-cutting off,
       so the row reads as continuing infinitely rather than stopping
       at a wall. Implemented as two gradient scrims layered over the
       carousel (not a CSS mask on the 3D scene itself, which would
       clip overflowing off-centre cards at the box edge instead of
       fading them) — pointer-events:none so they never block drag/
       click. z-index kept LOW (5): the carousel's cards live in their
       own isolated stacking context (#fan-layout has transform-style:
       preserve-3d, which creates one), so from outside that context
       the whole card stack only ever counts as a single layer at
       .fan-wrap's stacking level — the scrims only need to clear
       that, not the cards' internal 0-2000 z-index range. Using a
       high value here (previously 2100) also beat the detail-modal
       backdrop's z-index:1000, so the scrims kept showing through
       un-dimmed/un-blurred over the modal — the actual bug this fixes.
       Color matches the Projects sheet's own background (#E4F0F6) so
       the fade reads as "into the page", not as a visible grey bar.

       Anchored to .works-projects (the full-bleed 100%-height panel),
       NOT .fan-wrap — .fan-wrap is only as tall as the carousel itself,
       so top:0/bottom:0 there made the fade a "floating box" with
       visible hard top/bottom edges instead of a strip running the
       full height of the section. .works-projects is position:absolute
       with height:100% (see its shared rule with .works-activities
       above), so top:0/bottom:0 here spans the whole panel, matching
       the section's actual background edge to edge. ── */
    #works .works-projects::before,
    #works .works-projects::after {
      content: '';
      position: absolute;
      top: 0;
      bottom: 0;
      width: clamp(48px, 9vw, 140px);
      z-index: 5;
      pointer-events: none;
    }
    #works .works-projects::before {
      left: 0;
      background: linear-gradient(to right, #E4F0F6, rgba(228,240,246,0));
    }
    #works .works-projects::after {
      right: 0;
      background: linear-gradient(to left, #E4F0F6, rgba(228,240,246,0));
    }
    @media (max-width: 900px) {
      #works .works-projects::before,
      #works .works-projects::after { width: clamp(24px, 6vw, 64px); }
    }

    /* ── PROJECT INFO — centered readout beneath the carousel; content
       is swapped by works-fan-carousel.js (updateInfoPanel) every time
       focus changes, with a short fade+rise so the swap reads as a
       deliberate transition rather than a jump-cut. ── */
    .fan-info {
      flex: 0 0 auto;
      width: 100%;
      max-width: 460px;
      color: #0A0F1E;
      text-align: center;
    }
    .fan-info-index {
      display: none;
    }

    /* ── EYEBROW — "PROJECT 03" (mono, editorial-catalog numbering)
       + category pill, sitting well above the title so it reads as a
       case-study kicker rather than competing with the headline. ── */
    .fan-info-eyebrow {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 12px;
      margin-bottom: 14px;
    }
    .fan-info-num {
      font-family: 'JetBrains Mono', monospace;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.14em;
      color: rgba(10,15,30,.45);
    }
    .fan-info-cat-pill {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      padding: 5px 11px;
      border-radius: 99px;
    }

    .fan-info-title {
      /* Serif display face — matches the "Abyss"/"Frost" color-combo
         reference the palette itself (#E4F0F6/#0A0F1E) came from,
         instead of the site's usual grotesque/sans headings. */
      font-family: 'Playfair Display', 'Plus Jakarta Sans', serif !important;
      /* Final polish pass: was clamp(26px, 3vw, 38px) — title read a
         touch small next to the focal image. Nudged up so it lands
         in the 40-44px range on desktop widths (still responsive,
         same clamp shape, just scaled) and remains the strongest
         visual element after the active card itself. */
      font-size: clamp(28px, 3.5vw, 42px);
      line-height: 1.15;
      font-weight: 600;
      letter-spacing: -0.01em;
      color: #0A0F1E;
      margin: 0 0 6px;
    }
    .fan-info-desc {
      font-family: 'DM Sans', 'Plus Jakarta Sans', sans-serif;
      font-size: 14.5px;
      line-height: 1.75;
      color: rgba(10,15,30,.6);
      margin: 0 auto 22px;
      max-width: 42ch;
    }

    /* ── METADATA GRID — Type / Role / Year / Status. Deliberately
       tiny + muted (11px label, 13px value) so it reads as supporting
       detail under the title, never competing with it. A hairline
       top border stands in for a divider between the narrative
       (title+desc) and the spec-sheet (metadata) without adding a
       heavy <hr>. Rows with no data are filtered out in JS, and the
       whole block collapses via .is-empty when nothing is present. ── */
    .fan-info-meta {
      display: flex;
      flex-direction: column;
      gap: 10px;
      width: 100%;
      max-width: 280px;
      margin: 0 auto 26px;
      padding-top: 18px;
      border-top: 1px solid rgba(10,15,30,.1);
    }
    .fan-info-meta.is-empty { display: none; margin: 0; padding: 0; border: none; }
    .fan-info-meta-item {
      margin: 0;
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 16px;
    }
    .fan-info-meta-label {
      font-size: 10.5px;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: rgba(10,15,30,.4);
      margin: 0;
    }
    .fan-info-meta-value {
      font-family: 'DM Sans', 'Plus Jakarta Sans', sans-serif;
      font-size: 13.5px;
      font-weight: 600;
      color: #0A0F1E;
      margin: 0;
      text-align: right;
    }

    .fan-info-stack {
      display: flex; flex-wrap: wrap; justify-content: center; gap: 7px;
      margin-bottom: 24px;
    }
    .fan-info-tag {
      font-size: 11px; font-weight: 600;
      padding: 5px 11px; border-radius: 7px;
      background: rgba(10,15,30,.06);
      color: rgba(10,15,30,.72);
      border: 1px solid rgba(10,15,30,.1);
    }

    /* ── CTA row — editorial text links, not buttons: "View Project →"
       is the primary action, GitHub secondary. No fill/pill background
       so they read as inline text affordances under the metadata
       rather than competing with it visually; the only interactive
       cue is the underline and the arrow nudging right on hover. Each
       independently hidden when the field is missing so a project
       with only one link never leaves a stray gap. ── */
    .fan-info-actions {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 22px;
    }
    .fan-info-link,
    .fan-info-github {
      display: none;
      align-items: center;
      gap: 6px;
      width: fit-content;
      font-size: 13px;
      font-weight: 700;
      letter-spacing: 0.01em;
      padding: 2px 0;
      background: none;
      border: none;
      border-radius: 0;
      text-decoration: none;
      transition: opacity .25s var(--ease-soft, cubic-bezier(.22,1,.36,1));
    }
    .fan-info-link {
      color: #0A0F1E;
      border-bottom: 1px solid rgba(10,15,30,.28);
    }
    .fan-info-link-arrow {
      transition: transform .25s var(--ease-soft, cubic-bezier(.22,1,.36,1));
    }
    .fan-info-link.is-visible { display: inline-flex; }
    .fan-info-link:hover {
      opacity: .7;
    }
    .fan-info-link:hover .fan-info-link-arrow {
      transform: translateX(3px);
    }
    .fan-info-github {
      color: rgba(10,15,30,.55);
      border-bottom: 1px solid rgba(10,15,30,.14);
    }
    .fan-info-github.is-visible { display: inline-flex; }
    .fan-info-github:hover {
      opacity: .7;
      color: #0A0F1E;
    }

    /* ── content-swap transition — retriggered via classList remove/
       reflow/add in updateInfoPanel(), so every focus change gets a
       fresh fade+rise instead of relying on a single one-shot
       animation that only ever plays once. Direct children fade up
       in narrative order (eyebrow → title → desc → [meta] → stack →
       actions); the metadata grid itself is skipped here (opacity:1
       always) because its four rows animate individually below,
       continuing the same sequence rather than fading in as one
       block. ── */
    .fan-info.fi-swap > * {
      animation: fanInfoSwap .45s var(--ease-soft, cubic-bezier(.22,1,.36,1)) both;
    }
    .fan-info.fi-swap > *:nth-child(2) { animation-delay: .05s; }
    .fan-info.fi-swap > *:nth-child(3) { animation-delay: .10s; }
    .fan-info.fi-swap > .fan-info-meta { animation: none; opacity: 1; }
    .fan-info.fi-swap > *:nth-child(5) { animation-delay: .30s; }
    .fan-info.fi-swap > *:nth-child(6) { animation-delay: .36s; }
    /* metadata rows — sequential stagger of their own, picking up
       right after the description (child 3, .10s) and finishing
       before the tech-stack tags (child 5, .30s). */
    .fan-info.fi-swap .fan-info-meta-item {
      animation: fanInfoSwap .4s var(--ease-soft, cubic-bezier(.22,1,.36,1)) both;
    }
    .fan-info.fi-swap .fan-info-meta-item:nth-child(1) { animation-delay: .16s; }
    .fan-info.fi-swap .fan-info-meta-item:nth-child(2) { animation-delay: .19s; }
    .fan-info.fi-swap .fan-info-meta-item:nth-child(3) { animation-delay: .22s; }
    .fan-info.fi-swap .fan-info-meta-item:nth-child(4) { animation-delay: .25s; }
    @keyframes fanInfoSwap {
      from { opacity: 0; transform: translateY(8px); }
      to   { opacity: 1; transform: translateY(0); }
    }
    @media (prefers-reduced-motion: reduce) {
      .fan-info.fi-swap > *,
      .fan-info.fi-swap .fan-info-meta-item { animation: none; }
    }

    @media (max-width: 900px) {
      /* Narrow viewports just need a bit less breathing room; the
         layout itself (carousel on top, centered info below) is
         already the same stack used at every width. */
      #works .works-projects-layout { gap: 16px; }
      .fan-info { max-width: 100%; }
      .fan-info-desc { max-width: 100%; }
    }
    .depth-carousel {
      position: relative;
      width: 100%;
      max-width: 1200px;
      /* Was clamp(320px, 56vh, 560px) — sized off viewport HEIGHT, which
         on a typical laptop/desktop viewport landed at 500-560px while
         the cards themselves (see getCardSize() in works-fan-carousel.js,
         clamp(148px,22vw,260px) per side) only ever reach ~260px. Centered
         inside that much taller box, the cards left 100px+ of dead empty
         space above AND below themselves — which is exactly the "kenapa
         judulnya kejauh ke bawah" gap the user is pointing at. Sizing
         this off viewport WIDTH instead, matching the card formula plus
         a fixed allowance for the shadow-xl blur/drag headroom, keeps
         the box hugging the actual cards at every viewport size.
         (24vw + 56px) tracks 22vw+148..300 closely with ~50-60px of
         breathing room on each side — enough for the shadow to not clip
         and for a little drag travel, not a few hundred spare pixels.
         Ceiling raised to 360px (from 316px) alongside getCardSize()'s
         300px cap (was 260px) so the focused card can reach the
         ~280-300px focal size on wide viewports.
         Final polish pass: getCardSize()'s cap pulled back to 280px,
         so this ceiling/vw-term scale down with it proportionally
         (24vw→22vw, 360px→340px) — keeps the same breathing-room
         margin around the now slightly smaller card instead of
         leaving extra dead space. */
      height: clamp(204px, calc(22vw + 56px), 340px);
      display: flex;
      align-items: center;
      justify-content: center;
      /* perspective is also set inline by works-fan-carousel.js,
         scaled to the current card width so the coverflow rake stays
         proportional at any container size — this is just the
         pre-JS/no-JS fallback. */
      perspective: var(--dc-perspective, 1400px);
      perspective-origin: 50% 50%;
      touch-action: pan-y;
      outline: none;
      user-select: none;
      -webkit-user-select: none;
      cursor: grab;
    }
    .depth-carousel:active { cursor: grabbing; }
    .depth-carousel:focus-visible {
      outline: 2px solid rgba(255,255,255,.5);
      outline-offset: 4px;
      border-radius: 12px;
    }

    /* #fan-layout IS the .depth-carousel__stage — works-fan-carousel.js
       appends one .depth-carousel__card per project directly into it. */
    .depth-carousel__stage {
      position: absolute;
      inset: 0;
      transform-style: preserve-3d;
    }
    /* ── card — 1:1 match of the reference's own Tailwind classes:
       aspect-square, overflow-hidden, rounded-2xl (RADIUS=16px, set
       inline by works-fan-carousel.js), bg-muted. Depth-graded shadow
       (refinement pass): a soft, low base shadow on every card, with
       the focused card (.is-active, toggled per-frame in layout())
       stepping up to a stronger, premium soft shadow so it reads as
       the clear visual anchor — was previously a single static
       shadow-xl on every card regardless of focus. Transform/opacity/
       box-shadow are transition'd so drag-driven inline-style writes
       (transform/opacity, set every frame) stay snappy while the
       is-active shadow swap still eases instead of popping. ── */
    .depth-carousel__card {
      position: absolute;
      top: 50%; left: 50%;
      overflow: hidden;
      background: #f1f5f9; /* bg-muted */
      box-shadow: 0 10px 20px -10px rgba(0,0,0,.12);
      transition: box-shadow .4s var(--ease-soft, cubic-bezier(.22,1,.36,1));
      will-change: transform, opacity;
      cursor: pointer;
      transform: translate(-50%, -50%);
    }
    /* Focused/centre card — soft, premium lift matching the site's
       existing --shadow-lg character (rgba(15,23,42,...)) rather than
       introducing a new shadow color. */
    .depth-carousel__card.is-active {
      /* Final polish pass: was 0 20px 40px/.16 + 0 8px 18px/.08 — the
         double-stacked large-blur shadow read a bit like a generic
         "floating card" template. Trimmed blur/spread and opacity
         slightly so it stays a clear focal lift without looking
         heavy or templated. */
      box-shadow: 0 14px 28px rgba(15,23,42,.13), 0 4px 10px rgba(15,23,42,.06);
    }
    /* Subtle hover nudge on any visible, clickable card — a small
       lift, not a scale change (the per-distance scale is already
       driven every frame by layout()'s inline transform, so a CSS
       :hover transform here would fight it); kept out entirely under
       reduced-motion. */
    @media (hover: hover) and (pointer: fine) {
      .depth-carousel__card:not(.is-active):hover {
        box-shadow: 0 14px 26px -10px rgba(15,23,42,.18);
      }
    }
    .depth-carousel__img {
      width: 100%; height: 100%;
      object-fit: cover;
      display: block;
      pointer-events: none;
      -webkit-user-drag: none;
    }

    @media (prefers-reduced-motion: reduce) {
      .depth-carousel__card { will-change: auto; transition: none; }
    }

    /* ── loading skeleton — unrelated to the depth-rail layout above;
       just soft placeholder squares while data loads (phantom-ui
       shimmers over these), same pattern used elsewhere on the site. ── */
    #fan-stage phantom-ui {
      display: none;
      position: absolute; inset: 0;
      width: 100%; height: 100%;
      z-index: 2;
    }
    #fan-stage phantom-ui[loading] { display: block; }
    .fan-layout.is-loading-shell {
      position: absolute; inset: 0;
      display: flex; align-items: center; justify-content: center;
      flex-wrap: wrap; gap: 16px;
    }
    .fan-layout.is-loading-shell .fan-card {
      position: relative;
      width: 130px; height: 165px;
      border-radius: 18px;
      background: rgba(255,255,255,.06);
      box-shadow: none;
    }

    .fan-detail-overlay {
      display: none;
      opacity: 0;
      position: fixed;
      inset: 0;
      z-index: 1000;
      background: rgba(15, 23, 42, 0.45);
      /* No backdrop-filter here on the base state: computing a brand-new
         blurred backdrop the very first time an element is painted is
         what actually caused the leftover flicker (a "flash of sharp
         backdrop → blurred backdrop" the browser renders as one extra
         frame). It's added only once .open is present — see below —
         and by then the overlay has already been visible+painted for a
         frame via .is-visible, so the blur has nothing sudden to jump
         from. Also switched from CSS @keyframes (fadeIn/scaleIn, which
         race with the very display:none → block change that starts
         them) to plain transitions driven by a JS-buffered class swap;
         see showDetail() in works-fan-carousel.js. */
      transition: opacity .25s var(--ease-soft, ease), backdrop-filter .25s var(--ease-soft, ease);
      -webkit-transition: opacity .25s var(--ease-soft, ease), -webkit-backdrop-filter .25s var(--ease-soft, ease);
      align-items: center;
      justify-content: center;
      padding: 24px;
    }
    /* Step 1 (same tick as opening): just becomes displayable, still
       transparent/unblurred. Step 2 (next frame, via JS rAF): .open is
       added on top, which is what actually animates opacity + blur in.
       Splitting these into two classes/frames is what guarantees the
       browser paints the "off" state at least once before it starts
       transitioning — without that guaranteed first paint, browsers are
       free to collapse both states into a single frame and you get a
       pop instead of a transition (the flicker). */
    .fan-detail-overlay.is-visible { display: flex; }
    .fan-detail-overlay.open {
      opacity: 1;
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
    }
    .fan-detail {
      position: relative;
      width: 100%;
      max-width: 700px;
      max-height: 88vh;
      overflow-y: auto;
      padding: 26px 28px 28px;
      border-radius: 22px;
      background: var(--surface, #fff);
      border: 1px solid var(--glass-border, #e2e6ee);
      box-shadow: 0 30px 80px -20px rgba(15,23,42,.35);
      opacity: 0;
      transform: scale(0.95);
      transition: opacity .3s var(--ease-soft, ease), transform .3s var(--ease-soft, ease);
    }
    .fan-detail-overlay.open .fan-detail {
      opacity: 1;
      transform: scale(1);
    }
    .fan-detail-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 16px;
      margin-bottom: 18px;
    }
    .fan-detail-head-text { min-width: 0; }
    .fan-detail #fan-detail-cat { margin-bottom: 12px; }
    .fan-detail-head-actions {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .fan-detail-close {
      flex-shrink: 0;
      width: 34px; height: 34px; border-radius: 50%;
      border: none; background: var(--surface-2, #f1f5f9);
      color: var(--text-secondary, #64748b);
      cursor: pointer; font-size: 13px;
      display: flex; align-items: center; justify-content: center;
      transition: background .25s ease, transform .25s ease;
    }
    .fan-detail-close:hover { background: #e2e8f0; transform: scale(1.08); }
    /* ── tombol kecil opsional untuk membuka link project eksternal
       (kalau ada) — pengganti tombol "Lihat Detail Lengkap" yang
       dulu membuka modal kedua; sekarang panel ini SATU-SATUNYA
       tampilan detail, jadi aksi tambahannya cukup ikon kecil ── */
    .fan-detail-link {
      flex-shrink: 0;
      width: 34px; height: 34px; border-radius: 50%;
      background: var(--surface-2, #f1f5f9);
      color: var(--text-secondary, #64748b);
      text-decoration: none;
      display: none;
      align-items: center; justify-content: center;
      transition: background .25s ease, color .25s ease, transform .25s ease;
    }
    .fan-detail-link.is-visible { display: flex; }
    .fan-detail-link:hover {
      background: #e2e8f0;
      color: var(--detail-color, #0f172a);
      transform: scale(1.08);
    }
    .fan-detail-title {
      font-size: 22px; font-weight: 800; letter-spacing: -0.01em;
      color: var(--detail-color, #0f172a);
      margin: 0;
    }
    .fan-detail-desc {
      font-size: 13.5px; color: var(--text-secondary, #64748b);
      line-height: 1.7; margin: 0 0 14px;
    }
    /* ── galeri foto: filmstrip panjang yang di-loop tanpa akhir,
       ditarik langsung pakai mouse/jari (bukan lagi 1 foto + dot).
       Semua foto ditaruh berjejer dalam satu track yang digandakan
       3x biar transisi loop-nya mulus — pas geser kelewat batas
       set tengah, posisinya di-wrap balik tanpa keliatan lompat. ── */
    .fan-detail-gallery { margin: 0 0 18px; }
    .fan-detail-gallery.is-empty { display: none; }
    .fdg-stage {
      position: relative;
      width: 100%;
      aspect-ratio: 16 / 9;
      max-height: 300px;
      border-radius: 16px;
      overflow: hidden;
      background: var(--surface-2, #f1f5f9);
      cursor: grab;
      user-select: none;
      -webkit-user-select: none;
    }
    .fdg-stage.is-dragging { cursor: grabbing; }
    .fdg-img {
      width: 100%; height: 100%;
      object-fit: cover;
      display: block;
      pointer-events: none;
      -webkit-user-drag: none;
    }
    @keyframes fdgFade { from { opacity: 0; } to { opacity: 1; } }
    /* ── filmstrip draggable & infinite loop ── */
    .fdg-stage.fdg-strip { touch-action: pan-y; }
    .fdg-track {
      position: absolute;
      top: 0; left: 0;
      height: 100%;
      display: flex;
      align-items: stretch;
      gap: 14px;
      will-change: transform;
    }
    .fdg-slide {
      position: relative;
      flex: 0 0 auto;
      width: 86%;
      height: 100%;
      border-radius: 16px;
      overflow: hidden;
      background: var(--surface-2, #f1f5f9);
    }
    .fdg-slide img {
      width: 100%; height: 100%;
      object-fit: cover;
      display: block;
      pointer-events: none;
      -webkit-user-drag: none;
      user-select: none;
      -webkit-user-select: none;
    }
    .fan-detail-stack {
      display: flex; flex-wrap: wrap; gap: 6px;
      margin-bottom: 0;
    }

    /* ── WORKS: typography hierarchy polish ──
       The global `body, * { font-family: DM Sans !important }` rule
       (see the redesign block at the top of <head>) silently overrides
       the Plus Jakarta Sans set on .fan-detail-title (works-hero-title
       has its own fix earlier, alongside .page-title), so the detail
       overlay's project title was rendering in the body font. Restoring
       Plus Jakarta Sans (with !important to win that specificity
       fight) keeps it a clear step in the section's heading hierarchy. */
    .fan-detail-title {
      font-family: 'Plus Jakarta Sans', sans-serif !important;
    }
    .fan-detail-title {
      font-size: clamp(19px, 2.6vw, 24px);
      line-height: 1.25;
    }

    /* ══════════════════════════════════════════════════════════
       WORKS GALLERY — "living workspace" motion layer
       (analisis: lihat riwayat chat). 100% aditif — tidak
       mengubah posisi/ukuran card, kolom masonry, atau warna
       dasar. CSS di sini hanya menyediakan LAPISAN (glow/glare/
       shadow-var) dan fallback non-JS; gerakan utama (idle float,
       tilt, lift) dikendalikan GSAP di premium.js supaya selaras
       dengan pola motion yang sudah dipakai di file itu.
       ══════════════════════════════════════════════════════════ */

    /* ── ambient glow: muncul di belakang card saat hover, menyebar
       ke background, bukan border glow — opacity rendah, warna
       mengikuti kategori project (--card-color) ── */
    .card-glow {
      position: absolute;
      inset: -20% -16%;
      border-radius: 30px;
      background: radial-gradient(closest-side, var(--card-color, #2563eb), transparent 72%);
      opacity: 0;
      filter: blur(30px);
      pointer-events: none;
      z-index: 0;
      transition: opacity .45s var(--ease-soft, cubic-bezier(.16,1,.3,1));
    }
    .card.is-hovered .card-glow { opacity: .22; }

    /* ── glass reflection: highlight tipis yang bergeser mengikuti
       posisi cursor lewat --gx/--gy, sangat subtle ── */
    .card-glare {
      position: absolute; inset: 0;
      z-index: 3;
      pointer-events: none;
      border-radius: inherit;
      background: radial-gradient(140px 100px at var(--gx, 50%) var(--gy, 15%),
        rgba(255,255,255,.38), rgba(255,255,255,0) 62%);
      opacity: 0;
      transition: opacity .35s ease;
      mix-blend-mode: overlay;
    }
    .card.is-hovered .card-glare { opacity: 1; }

    /* ── dynamic shadow: arah mengikuti posisi cursor lewat
       --shadow-x/--shadow-y (di-set JS saat mousemove) ── */
    .card-face {
      box-shadow:
        var(--shadow-x, 0px) calc(10px + var(--shadow-y, 0px)) 22px -10px rgba(15,23,42,.14),
        0 2px 6px rgba(15,23,42,.08);
      transition: box-shadow .4s var(--ease-soft);
    }
    .card.is-hovered .card-face {
      box-shadow:
        var(--shadow-x, 0px) calc(20px + var(--shadow-y, 0px)) 38px -12px rgba(15,23,42,.22),
        0 4px 10px rgba(15,23,42,.1);
    }

    /* ── connection effect: boost sementara pada ambient spotlight
       global (#bg-spotlight, sudah ada di premium.js) saat sebuah
       card di-hover — efeknya otomatis terlokalisir di sekitar
       card karena spotlight mengikuti posisi cursor asli ── */
    #bg-spotlight-boost {
      position: fixed; inset: 0; z-index: 5; pointer-events: none;
      background: radial-gradient(720px 720px at var(--mx) var(--my),
        rgba(37, 99, 235,.14), rgba(99,102,241,.06) 42%, transparent 70%);
      opacity: 0;
      transition: opacity .5s ease;
      mix-blend-mode: multiply;
    }
    body.spotlight-boost #bg-spotlight-boost { opacity: 1; }
    @media (max-width: 768px), (prefers-reduced-motion: reduce) {
      #bg-spotlight-boost { display: none; }
    }

    /* ── background "breathing": pulse opacity sangat pelan pada
       pattern network yang sudah ada, supaya terasa hidup tanpa
       mengubah bentuk/posisi node sama sekali ── */
    body::before {
      content: '';
      position: fixed; inset: 0;
      z-index: 1;
      pointer-events: none;
      background-image: inherit;
      background-repeat: repeat;
      background-size: 600px 600px;
      background-position: 0 0;
      opacity: 0;
      animation: bgBreathe 11s ease-in-out infinite;
    }
    @keyframes bgBreathe {
      0%, 100% { opacity: 0; }
      50%      { opacity: .5; }
    }
    @media (prefers-reduced-motion: reduce) {
      body::before { animation: none; display: none; }
    }

    .spl-item {
      display: flex; align-items: center; gap: 11px;
      padding: 11px 13px;
      border-radius: 13px;
      border: 1px solid #e8edf3;
      background: #fff;
      cursor: pointer;
      transition: border-color .25s cubic-bezier(.22,1,.36,1),
                  background .25s cubic-bezier(.22,1,.36,1),
                  transform .25s cubic-bezier(.22,1,.36,1),
                  box-shadow .25s cubic-bezier(.22,1,.36,1);
      position: relative;
      overflow: hidden;
    }
    .spl-item:hover {
      border-color: #c7d2e0;
      transform: translateX(3px);
      box-shadow: 0 6px 16px -6px rgba(15,23,42,.10);
    }
    .spl-item.active {
      border-color: transparent;
      background: #f0f4ff;
      transform: translateX(3px) scale(1.015);
      box-shadow: 0 10px 24px -8px rgba(37, 99, 235,.18);
    }
    .spl-item.active::before {
      content: '';
      position: absolute; left: 0; top: 0; bottom: 0;
      width: 3px;
      border-radius: 3px 0 0 3px;
      transition: background .3s ease;
    }
    /* index number — encodes the project's position in the list */
    .spl-index {
      font-family: 'JetBrains Mono', monospace;
      font-size: 10px; font-weight: 700; color: #cbd5e1;
      width: 16px; flex-shrink: 0; text-align: right;
      transition: color .25s ease;
    }
    .spl-item.active .spl-index { color: var(--spl-color, #2563eb); }
    /* thumb kecil */
    .spl-thumb {
      width: 56px; height: 44px; border-radius: 9px;
      overflow: hidden; flex-shrink: 0;
      background: #0a0f1e;
      transition: transform .25s ease;
    }
    .spl-item.active .spl-thumb, .spl-item:hover .spl-thumb { transform: scale(1.04); }
    .spl-thumb img {
      width: 100%; height: 100%;
      object-fit: cover; object-position: center; display: block;
      transition: transform .4s ease;
    }
    .spl-thumb-placeholder {
      width: 100%; height: 100%;
      display: flex; align-items: center; justify-content: center;
    }
    .spl-info { flex: 1; min-width: 0; }
    .spl-title {
      font-size: 12px; font-weight: 600; color: #0f172a;
      line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .spl-sub {
      font-size: 10px; color: #64748b; margin-top: 2px;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .spl-badge {
      font-size: 9px; font-weight: 700; padding: 2px 7px;
      border-radius: 5px; flex-shrink: 0; letter-spacing: .04em;
    }
    /* category colors re-used */
    .bpill-iot   { background:rgba(37, 99, 235,0.15);  color:#3b82f6; }
    .bpill-ml    { background:rgba(124,58,237,0.15); color:#7c3aed; }
    .bpill-cv    { background:rgba(5,150,105,0.15);  color:#059669; }
    .bpill-web   { background:rgba(217,119,6,0.15);  color:#d97706; }
    .bpill-design{ background:rgba(219,39,119,0.15); color:#db2777; }

    /* ── MODAL: category tag pakai ikon + label ── */
    .modal-cat-tag {
      display: inline-flex !important;
      align-items: center;
      gap: 7px;
    }
    .modal-cat-tag svg { width: 14px; height: 14px; flex-shrink: 0; }
    .bdot-iot  { background:#3b82f6; }
    .bdot-ml   { background:#7c3aed; }
    .bdot-cv   { background:#059669; }
    .bdot-web  { background:#d97706; }
    .bdot-design{ background:#db2777; }
    .bbg-iot   { background:linear-gradient(135deg,#dbeafe,#eff6ff); }
    .bbg-ml    { background:linear-gradient(135deg,#ede9fe,#f5f3ff); }
    .bbg-cv    { background:linear-gradient(135deg,#d1fae5,#ecfdf5); }
    .bbg-web   { background:linear-gradient(135deg,#fef3c7,#fffbeb); }
    .bbg-design{ background:linear-gradient(135deg,#fce7f3,#fdf2f8); }
    .bbar-iot  { background:linear-gradient(90deg,#2563eb,#60a5fa); }
    .bbar-ml   { background:linear-gradient(90deg,#7c3aed,#a78bfa); }
    .bbar-cv   { background:linear-gradient(90deg,#059669,#34d399); }
    .bbar-web  { background:linear-gradient(90deg,#d97706,#fcd34d); }
    .bbar-design{ background:linear-gradient(90deg,#db2777,#f472b6); }

    /* ── FIX: navbar sizing ── */
    nav.nav-fix {
      max-width: fit-content;
      margin: 0 auto;
      padding: 6px 8px;
    }
    nav.nav-fix .nav-links {
      display: flex;
      align-items: center;
      gap: 2px;
      margin: 0;
    }
    nav.nav-fix .nav-links a {
      display: flex;
      align-items: center;
      white-space: nowrap;
      padding: 9px 16px;
      font-size: 14.5px;
    }
    @media (max-width: 640px) {
      nav.nav-fix {
        max-width: 94vw;
        padding: 6px 6px;
      }
      nav.nav-fix .nav-links {
        gap: 2px;
        justify-content: flex-start;
        width: 100%;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
      }
      nav.nav-fix .nav-links::-webkit-scrollbar { display: none; }
      nav.nav-fix .nav-links li { flex: 0 0 auto; }
      nav.nav-fix .nav-links a {
        padding: 8px 14px;
        font-size: 12.5px;
        text-align: center;
      }

      /* ── Floating card layout on mobile: navbar + konten disatukan jadi
         SATU bentuk kartu mengambang yang menyambung (tanpa jarak),
         background pattern tetap terlihat di sekelilingnya ── */

      nav.nav-fix {
        top: 14px !important;
        height: 72px !important;
        box-sizing: border-box !important;
        border-radius: 26px 26px 0 0 !important;
        background: var(--card-bg) !important;
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
        border: 1px solid var(--glass-border) !important;
        border-bottom: none !important;
        box-shadow: var(--shadow-lg) !important;
      }

      body > section[id] {
        position: fixed !important;
        top: 86px !important;
        left: 14px !important;
        right: 14px !important;
        bottom: max(14px, env(safe-area-inset-bottom)) !important;
        width: auto !important;
        height: auto !important;
        margin: 0 !important;
        background: var(--card-bg) !important;
        border-radius: 0 0 26px 26px !important;
        box-shadow: var(--shadow-lg) !important;
        border: 1px solid var(--glass-border) !important;
        border-top: none !important;
        padding: 22px 18px 28px !important;
        overflow-y: auto !important;
        overflow-x: hidden !important;
        -webkit-overflow-scrolling: touch;
        transition: background .4s var(--ease-soft, ease);
      }

      /* Samakan margin kiri/kanan nav persis dengan kartu, biar sejajar menyambung */
      nav.nav-fix {
        max-width: none !important;
        left: 14px !important;
        right: 14px !important;
        margin-left: 0 !important;
        margin-right: 0 !important;
      }

    }

    /* Label judul (PORTFOLIO/Selected Works, CREDENTIALS/Certificate Board)
       & jumlahnya disembunyikan di SEMUA layar —
       sudah jelas dari nav aktif, gak perlu diulang */
    #works .page-header,
    #certificate .page-header,
    #works > hr.divider,
    #certificate > hr.divider {
      display: none !important;
    }

    /* ── FIX: beberapa section didesain untuk pas 1 layar penuh (100vh) dengan
       overflow:hidden di desktop. Di kartu mobile yang lebih kecil, asumsi itu
       bikin konten ketabrak/kepotong & gak bisa discroll. Netralkan di sini. ── */
    @media (max-width: 640px) {
      /* Home: hero jangan dipaksa setinggi 1 layar, biar rapi & gak nyisa ruang kosong */
      .hero {
        min-height: auto !important;
        padding: 8px 0 24px !important;
      }

      /* Works: matikan mode "muat 1 layar" + wheel-jack slider, biarkan
         mengalir & bisa discroll penuh (Projects lalu Activities, biasa,
         document-order) — lihat juga guard mq.matches di JS slider yang
         no-op begitu masuk breakpoint ini. */
      #works {
        height: auto !important;
        max-height: none !important;
        overflow: visible !important;
        /* Restore the standard nav-clearance padding here — the
           padding-top:0 override on the base #works rule is only
           meant for the desktop wheel-jack sheets (which supply their
           own internal clearance instead, see .works-projects /
           .works-activities-hero); mobile drops the jacking entirely
           and goes back to plain static/stacked flow, which still
           needs #works itself to reserve the usual 108px. */
        padding-top: 108px !important;
      }
      .works-views {
        position: static !important;
        overflow: visible !important;
      }
      .works-projects,
      .works-activities {
        position: static !important;
        inset: auto !important;
        width: auto !important;
        height: auto !important;
        overflow: visible !important;
        opacity: 1 !important;
        transform: none !important;
        pointer-events: auto !important;
      }
      .works-projects {
        padding: 20px 0 22px !important;
      }
      .works-activities {
        padding-bottom: 4px !important;
      }

      /* Activities chapter: cegah board ikut collapse saat #works jadi auto-height */
      .masonry-wrap {
        flex: none !important;
        padding-top: 6px !important;
      }

      /* Certificate: kasih sedikit padding atas pengganti header yang disembunyikan */
      #certificate .board-wrap {
        padding-top: 6px !important;
      }

      /* ── Works: tighten rhythm on small screens so the hero and
         stage don't eat the limited vertical space, and the detail
         panel stays comfortable to read one-handed. ── */
      #works .works-hero { padding-bottom: 12px; }
      /* Reset the top padding this rule gets on desktop (where it
         doubles as the wheel-jack sheet's own nav-clearance spacer,
         see the base rule) — mobile already gets that clearance from
         #works's own restored padding-top just above, so keeping both
         would double up into a ~220px empty gap before the grid. */
      .works-activities-hero { padding-top: 0; padding-bottom: 12px; }
      .msnry-shuffle-btn { top: 16px; }
      .works-subhead { padding: 0 5vw 10px; }
      .fan-detail { padding: 22px 20px 24px; border-radius: 20px; }
      .fan-detail-title { font-size: clamp(18px, 5vw, 21px); }
      .fan-detail-head { margin-bottom: 14px; }
      .fan-detail-gallery { margin-bottom: 14px; }
    }
/* ════════════════════════════════════════════════════════════════
   PREMIUM FLOATING NAV CONTROLS — redesigned prev/next arrows
   (.lb-arr — Activities lightbox photo slideshow, the live control;
    .cs-arr — card slideshow arrows, kept in sync for consistency).
   Placed last on purpose so it wins over both the earlier glass pass
   (~L753/L1153) and the later neumorphic pass (~L1429/L1455) without
   needing !important anywhere. Apple Vision Pro / Linear / Framer /
   Raycast inspired: glass disc, soft blur, hairline border, restrained
   motion. Functionality (lbSlide()/cardSlide() onclick, arrow-key
   nav) is untouched — this only replaces the visual layer plus adds
   ripple, magnetic-proximity opacity and a directional icon nudge.
   ════════════════════════════════════════════════════════════════ */

.lb-arr, .cs-arr {
  --nav-size: 40px;
  --nav-glow: 96, 165, 250;      /* soft blue, matches --primary/--blue */
  --nav-tint: 255, 255, 255;     /* glass base tint */
  --nav-fg: 255, 255, 255;
  --nav-proximity: 0.82;

  position: absolute;
  top: 50%;
  margin-top: calc(var(--nav-size) / -2);
  width: var(--nav-size);
  height: var(--nav-size);
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  border-radius: 50%;
  color: rgb(var(--nav-fg));
  cursor: pointer;
  opacity: var(--nav-proximity);
  -webkit-tap-highlight-color: transparent;
  transition: opacity .35s var(--ease-soft, ease);
}
.lb-prev, .cs-prev { left: 16px; }
.lb-next, .cs-next { right: 16px; }
.cs-arr { --nav-size: 32px; --nav-fg: 15, 23, 42; }
.cs-prev { left: 8px; } .cs-next { right: 8px; }

/* Glass disc lives on ::before so scale/press animate independently
   of the button's own transform — the button element itself is left
   free for the magnetic x/y translate (premium.js, quickTo) to own
   without the two fighting over the same `transform` property. */
.lb-arr::before, .cs-arr::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: rgba(var(--nav-tint), 0.10);
  border: 1px solid rgba(var(--nav-tint), 0.20);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  box-shadow:
    0 8px 22px rgba(0, 0, 0, 0.22),
    inset 0 1px 0 rgba(var(--nav-tint), 0.16);
  transform: scale(1);
  transform-origin: center;
  transition:
    transform .45s var(--ease-soft, ease),
    background .35s var(--ease-soft, ease),
    border-color .35s var(--ease-soft, ease),
    box-shadow .35s var(--ease-soft, ease);
}
.cs-arr::before {
  background: rgba(var(--nav-tint), 0.55);
  box-shadow:
    0 6px 16px rgba(15, 23, 42, 0.10),
    inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

/* Ambient glow — soft halo that blooms outward on hover/focus, sits
   behind the disc (negative z-index within the button's own stacking
   context, established by z-index:10 above). */
.lb-arr::after, .cs-arr::after {
  content: "";
  position: absolute;
  inset: -12px;
  border-radius: 50%;
  z-index: -1;
  background: radial-gradient(circle, rgba(var(--nav-glow), 0.45), transparent 72%);
  opacity: 0;
  transition: opacity .4s var(--ease-soft, ease);
  pointer-events: none;
}

.lb-arr-icon, .cs-arr .cs-arr-icon {
  position: relative;
  z-index: 1;
  display: block;
  transition: transform .35s var(--ease-soft, ease);
}
.cs-arr { font-size: 15px; }

.lb-arr:hover, .lb-arr:focus-visible,
.cs-arr:hover, .cs-arr:focus-visible {
  opacity: 1;
}
.lb-arr:hover::before, .lb-arr:focus-visible::before,
.cs-arr:hover::before, .cs-arr:focus-visible::before {
  transform: scale(1.08);
  background: rgba(var(--nav-tint), 0.16);
  border-color: rgba(var(--nav-glow), 0.45);
  box-shadow:
    0 10px 28px rgba(0, 0, 0, 0.26),
    0 0 0 1px rgba(var(--nav-glow), 0.14),
    inset 0 1px 0 rgba(var(--nav-tint), 0.22);
}
.lb-arr:hover::after, .lb-arr:focus-visible::after,
.cs-arr:hover::after, .cs-arr:focus-visible::after {
  opacity: 1;
}
.lb-prev:hover .lb-arr-icon, .lb-prev:focus-visible .lb-arr-icon,
.cs-prev:hover .cs-arr-icon, .cs-prev:focus-visible .cs-arr-icon {
  transform: translateX(-2.5px);
}
.lb-next:hover .lb-arr-icon, .lb-next:focus-visible .lb-arr-icon,
.cs-next:hover .cs-arr-icon, .cs-next:focus-visible .cs-arr-icon {
  transform: translateX(2.5px);
}

/* Press feedback — snappy down, soft spring back via cubic-bezier
   overshoot rather than literal spring physics (kept in CSS so it
   never competes with the GSAP-driven magnetic transform). */
.lb-arr:active::before, .cs-arr:active::before {
  transform: scale(0.90);
  transition: transform .15s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Keyboard focus — always visible, never relies on hover. */
.lb-arr:focus-visible, .cs-arr:focus-visible {
  outline: none;
}
.lb-arr:focus-visible::before {
  box-shadow:
    0 0 0 2px rgba(15, 23, 42, 0.92),
    0 0 0 4px rgba(var(--nav-glow), 0.75),
    0 10px 28px rgba(0, 0, 0, 0.26);
}
.cs-arr:focus-visible::before {
  box-shadow:
    0 0 0 2px #fff,
    0 0 0 4px rgba(var(--nav-glow), 0.75),
    0 6px 16px rgba(15, 23, 42, 0.10);
}

/* Ripple — one soft pulse per click/tap, injected by premium.js and
   removed on animationend so it never accumulates in the DOM. */
.nav-ripple {
  position: absolute;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: rgba(var(--nav-tint), 0.65);
  transform: scale(0);
  opacity: 0.55;
  pointer-events: none;
  z-index: 2;
  animation: navRippleAnim .6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes navRippleAnim {
  to { transform: scale(9); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lb-arr, .cs-arr, .lb-arr::before, .cs-arr::before,
  .lb-arr::after, .cs-arr::after, .lb-arr-icon, .cs-arr-icon {
    transition: none !important;
  }
  .nav-ripple { animation: none !important; display: none; }
}

@media (max-width: 640px) {
  .lb-arr { --nav-size: 36px; }
  .lb-prev { left: 10px; } .lb-next { right: 10px; }
}

/* ════════════════════════════════════════════════════════════════
   WORKS panel — plain, static background matching the reference
   component's own `bg-background` demo wrapper. The old per-project
   accent-color system (dynamic --accent/--accent-glow/--accent-shadow
   custom properties + the .works-ambient radial-gradient layers that
   tracked the carousel's drag position) has been removed entirely —
   the reference has no such thing, the panel just sits there as one
   flat color the whole time. ════════════════════════════════════════ */
#works-view-projects {
  position: relative;
  overflow: hidden;
  background: #FAFAF8;
}

/* Info panel accents — fixed neutral tones now (no longer driven by
   a live per-project accent variable). */
.fan-info-tag {
  border-color: rgba(10,15,30,.14);
}
/* ════════════════════════════════════════════════════════════════
   COLOR WIPE PAGE TRANSITION — fullscreen multi-colour grid curtain
   that sweeps in from BOTH sides between switchPage() navigations
   (see color-wipe-transition.js). The layer is divided into
   horizontal rows; each row holds two `.cw-stripe` blocks (built/
   coloured from the page's live palette in the JS) that slide in
   from the left and right edges and meet at a split point, together
   covering the row. Once every row is covered the route swaps, then
   every block keeps travelling in the same direction it entered
   from to clear the screen and reveal the destination page. Each
   block's top/height/left/width is set inline per-row by the JS —
   this stylesheet only owns the shared transition timing. Still
   pure `transform: translateX` per block (GPU-composited) — no
   blur, no filters, no particles, no layout-triggering properties.
   ════════════════════════════════════════════════════════════════ */
.cw-layer {
  position: fixed;
  inset: 0;
  z-index: 99990; /* above navbar iframe (1000) & modals (999) */
  overflow: hidden;
  visibility: hidden;
  pointer-events: none;
}
.cw-layer.cw-visible {
  visibility: visible;
  pointer-events: auto; /* block input on the covered page mid-wipe */
}
.cw-stripe {
  position: absolute;
  /* top/height/left/width are set inline per block in the JS —
     each row's two blocks size themselves around their split point. */
  transform: translateX(-100vw); /* pre-JS fallback position */
  will-change: transform;
  backface-visibility: hidden;
  /* --cw-dur/--cw-ease are shared, set once per sweep leg on .cw-layer
     and inherited; --cw-delay is per-row, set inline in the JS. */
  transition: transform var(--cw-dur, 520ms)
              var(--cw-ease, cubic-bezier(.55, .055, .675, .19))
              var(--cw-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  .cw-stripe { transition: none !important; }
}

/* ══════════════════════════════════════════════════════════════════
   REACT KINETIC PAGE TRANSITION — replaces the color-wipe rules above
   (kept in place, unused, in case of rollback). Layers are driven
   entirely by GSAP (transform: translateX via the `x` prop) from
   react-page-transition.js; this block only establishes their box,
   stacking, and idle state. Same z-index tier as the old .cw-layer:
   above the navbar iframe (1000) & modals (999)
   (999999).
   ══════════════════════════════════════════════════════════════════ */
#rpt-root {
  position: fixed;
  inset: 0;
  z-index: 99990;
  visibility: hidden;
  pointer-events: none;
  overflow: hidden;
}
/* Each .rpt-layer is one BLOCK in the row grid, not a full-viewport
   panel — top/height/left/width are set inline per block by
   react-page-transition.js (a row per pair of live colours extracted
   from the departing page, each row split into a left block + a
   right block that enter from opposite edges). */
.rpt-layer {
  position: absolute;
  transform: translateX(101vw); /* pre-JS fallback position, GSAP takes over via the `x` prop */
  will-change: transform;
  backface-visibility: hidden;
}
/* ══════════════════════════════════════════════════════════
   EMAIL COPY TOAST — shown when an .email-action icon is clicked
   ══════════════════════════════════════════════════════════ */
.email-toast {
  position: fixed;
  left: 50%;
  bottom: 32px;
  transform: translateX(-50%) translateY(16px);
  z-index: 9999;
  background: var(--dark, #0f172a);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 12px 20px;
  border-radius: 999px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s var(--ease-soft, ease), transform 0.25s var(--ease-soft, ease);
  white-space: nowrap;
  max-width: 90vw;
  overflow: hidden;
  text-overflow: ellipsis;
}

.email-toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@media (max-width: 480px) {
  .email-toast {
    font-size: 12.5px;
    padding: 10px 16px;
    bottom: 20px;
    max-width: 86vw;
  }
}

/* ════════════════════════════════════════════════════════════════
   INTERACTIVE BENTO GALLERY — dipakai #activity-gallery di section
   Achievements. Terjemahan langsung dari utility Tailwind komponen
   React "interactive-bento-gallery.tsx" (lihat bento-gallery.js);
   nama kelas & nilainya dijaga sepadan supaya desainnya persis sama:

     .bg-grid       ← grid-cols-1 sm:grid-cols-3 md:grid-cols-4
                      gap-3 auto-rows-[60px]
     .bg-span-*     ← md:col-span-N md:row-span-N sm:col-span-N …
     .bg-modal      ← fixed inset-0 backdrop-blur-lg …
     .bg-dock       ← fixed left-1/2 bottom-4 -translate-x-1/2
   ════════════════════════════════════════════════════════════════ */

/* container mx-auto px-4 py-8 max-w-4xl */
.bento-gallery {
  width: 100%;
  max-width: 56rem;      /* max-w-4xl */
  margin-inline: auto;
  padding: 2rem 1rem;    /* py-8 px-4 */
  box-sizing: border-box;
}

/* mb-8 text-center */
.bento-gallery .bg-head {
  margin-bottom: 2rem;
  text-align: center;
}
/* text-2xl sm:text-3xl md:text-4xl font-bold + gradient text */
.bento-gallery .bg-heading {
  margin: 0;
  font-family: "Plus Jakarta Sans", "DM Sans", system-ui, sans-serif;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  background-image: linear-gradient(to right, #111827, #1f2937, #111827);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* mt-2 text-sm sm:text-base text-gray-600 */
.bento-gallery .bg-sub {
  margin: 0.5rem 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: #4b5563;
}
@media (min-width: 640px) {
  .bento-gallery .bg-heading { font-size: 1.875rem; line-height: 2.25rem; }
  .bento-gallery .bg-sub     { font-size: 1rem; }
}
@media (min-width: 768px) {
  .bento-gallery .bg-heading { font-size: 2.25rem; line-height: 2.5rem; }
}

/* ── GRID ── */
.bento-gallery .bg-grid {
  display: grid;
  grid-template-columns: repeat(1, minmax(0, 1fr));
  grid-auto-rows: 60px;
  gap: 0.75rem;          /* gap-3 */
  /* dense: begitu kartu punya tiga bentuk dengan tinggi berbeda,
     penempatan berurutan biasa meninggalkan lubang di grid (terlihat
     jelas di board: satu kolom kosong menganga di kanan). `dense`
     menyuruh browser mundur mengisi celah yang tertinggal. */
  grid-auto-flow: row dense;
}
.bento-gallery .bg-grid.is-hidden { visibility: hidden; }
@media (min-width: 640px) {
  .bento-gallery .bg-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 768px) {
  .bento-gallery .bg-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ── BENTUK KARTU ──
   Demo aslinya cuma punya dua bentuk, "tall" (1×3) dan "wide" (2×2),
   dan membagikannya selang-seling per indeks. Bentuknya jadi tidak
   ada hubungannya dengan foto yang masuk: foto potret bisa mendarat
   di kartu 2 kolom yang sangat melebar, dan yang kelihatan tinggal
   latar blur-nya.

   Sekarang ada TIGA bentuk, dipilih dari rasio asli tiap foto (lihat
   spanFor() di index.html), dengan rasio kartu yang dibuat mendekati
   rasio foto yang mengisinya:

     portrait   1 kol × 4 baris  ≈ 0.78   foto potret / HP
     square     1 kol × 3 baris  ≈ 1.05   foto persegi-an
     landscape  2 kol × 4 baris  ≈ 1.59   foto lanskap / 16:9

   Default .bg-card 1×2 tetap jadi fallback kalau span kosong. */
.bento-gallery .bg-card { grid-column: span 1 / span 1; grid-row: span 2 / span 2; }
@media (min-width: 640px) {
  .bento-gallery .bg-span-portrait  { grid-column: span 1 / span 1; grid-row: span 3 / span 3; }
  .bento-gallery .bg-span-square    { grid-column: span 1 / span 1; grid-row: span 2 / span 2; }
  .bento-gallery .bg-span-landscape { grid-column: span 2 / span 2; grid-row: span 3 / span 3; }
}
@media (min-width: 768px) {
  .bento-gallery .bg-span-portrait  { grid-column: span 1 / span 1; grid-row: span 4 / span 4; }
  .bento-gallery .bg-span-square    { grid-column: span 1 / span 1; grid-row: span 3 / span 3; }
  .bento-gallery .bg-span-landscape { grid-column: span 2 / span 2; grid-row: span 4 / span 4; }
}

/* ── KARTU ── relative overflow-hidden rounded-xl cursor-move */
.bento-gallery .bg-card {
  position: relative;
  overflow: hidden;
  border-radius: 0.75rem;
  cursor: move;
  /* warna kartu sebelum fotonya termuat — sedikit lebih gelap dari
     kertas kraft board (--paper #ece0c2) supaya kartu terbaca sebagai
     bidang yang tenggelam di atasnya, bukan tambalan warna asing */
  background: #ddd0ae;
  /* pan-y, BUKAN none: framer-motion memasang touch-action:none untuk
     tiap elemen `drag`, tapi di layar sentuh grid ini selebar layar —
     kalau none, swipe vertikal di atas galeri tidak bisa men-scroll
     halaman sama sekali dan user terjebak di sini. pan-y menyerahkan
     gerakan vertikal ke scroll halaman dan menyisakan gerakan
     horizontal untuk drag-reorder. Pola yang sama sudah dipakai
     .fdg-stage di file ini. */
  touch-action: pan-y;
  will-change: transform;
  transition: box-shadow 0.25s var(--ease-soft);
}
.bento-gallery .bg-card.is-dragging {
  z-index: 20;
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.28);
}
/* `contain`, BUKAN `cover`: kartu bento tingginya dikunci row-span
   sedangkan foto aktivitas rasionya macam-macam (potret HP, lanskap
   kamera, screenshot). Dengan cover, sisi panjang foto dipotong —
   wajah orang di pinggir frame atau teks di bawah spanduk kegiatan
   hilang begitu saja. contain menampilkan foto UTUH; pita kosong yang
   tersisa ditutup .bg-fill di bawah ini. */
.bento-gallery .bg-card-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: transform 0.35s var(--ease-soft);
}

/* Latar kartu: salinan foto itu sendiri, di-zoom + blur. Tanpa ini,
   foto yang rasionya tidak pas meninggalkan pita kosong di kiri-kanan
   atau atas-bawah dan grid terlihat bolong. Dengan ini kartu tetap
   terbaca sebagai satu blok warna penuh, tapi tidak ada satu piksel
   pun dari foto aslinya yang terpotong. */
.bento-gallery .bg-fill,
.bg-modal-root .bg-fill {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  filter: blur(22px) saturate(1.25);
  transform: scale(1.2);   /* sembunyikan tepi blur yang menerawang */
  opacity: 0.6;
  pointer-events: none;
}
/* whileHover={{ scale: 1.02 }} */
@media (hover: hover) and (pointer: fine) {
  .bento-gallery .bg-card:hover .bg-card-media { transform: scale(1.02); }
}

/* overlay judul/desc — opacity 0 → 1 saat hover (duration .2) */
.bento-gallery .bg-card-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 0.5rem;
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .bento-gallery .bg-card:hover .bg-card-overlay { opacity: 1; }
}
/* di layar sentuh tidak ada hover — tampilkan supaya judul tetap terbaca */
@media (hover: none) {
  .bento-gallery .bg-card-overlay { opacity: 1; }
}
@media (min-width: 640px) { .bento-gallery .bg-card-overlay { padding: 0.75rem; } }
@media (min-width: 768px) { .bento-gallery .bg-card-overlay { padding: 1rem; } }

/* bg-gradient-to-t from-black/80 via-black/40 to-transparent */
.bento-gallery .bg-card-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.4) 50%, transparent);
}
.bento-gallery .bg-card-title {
  position: relative;
  margin: 0;
  color: #fff;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.bento-gallery .bg-card-desc {
  position: relative;
  margin: 0.125rem 0 0;
  color: rgba(255, 255, 255, 0.7);
  font-size: 0.625rem;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
@media (min-width: 640px) {
  .bento-gallery .bg-card-title { font-size: 0.875rem; }
  .bento-gallery .bg-card-desc  { font-size: 0.75rem; }
}
@media (min-width: 768px) {
  .bento-gallery .bg-card-title { font-size: 1rem; }
  .bento-gallery .bg-card-desc  { font-size: 0.875rem; }
}

/* skeleton saat data belum tiba */
.bento-gallery .bg-card--skeleton {
  cursor: default;
  /* shimmer skeleton, warna krem seirama board (dulu #e9edf3/#f4f7fa
     yang abu kebiruan) */
  background: linear-gradient(100deg, #e0d3b2 30%, #f4ead2 50%, #e0d3b2 70%);
  background-size: 200% 100%;
  animation: bgSkeleton 1.4s ease-in-out infinite;
}
@keyframes bgSkeleton {
  from { background-position: 150% 0; }
  to   { background-position: -50% 0; }
}

/* ── MEDIA (image / video) ──
   CATATAN object-fit: rule ini SENGAJA tidak lagi menyetel object-fit.
   Sebelumnya di sini ada `object-fit: cover`, dan karena selektornya
   (.bg-modal-root .bg-media--img = 0,2,0) lebih spesifik daripada
   .bg-modal-media (0,1,0), foto di dalam modal ikut ter-crop meski
   .bg-modal-media sudah minta `contain`. Sekarang object-fit disetel
   eksplisit per konteks — kartu grid, modal, dan thumbnail dock —
   supaya tidak ada lagi jebakan spesifisitas seperti itu. */
.bento-gallery .bg-media--img,
.bg-modal-root .bg-media--img { cursor: pointer; user-select: none; }
.bento-gallery .bg-media--video,
.bg-modal-root .bg-media--video { position: relative; overflow: hidden; background: #0f172a; }
.bg-media-video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform: translateZ(0);
  will-change: transform;
  transition: opacity 0.2s;
}
/* Thumbnail dock ukurannya cuma 32–40px: di situ `contain` justru
   menyusutkan gambar jadi garis tipis, jadi khusus dock tetap cover. */
.bg-dock-thumb-media .bg-media-video { object-fit: cover; }
.bg-media-buffer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.1);
}
.bg-media-spinner {
  width: 1.5rem;
  height: 1.5rem;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  border-radius: 9999px;
  animation: bgSpin 1s linear infinite;
}
@keyframes bgSpin { to { transform: rotate(360deg); } }

/* ── MODAL ── */
body.bg-modal-open { overflow: hidden; }

/* z-index sama dengan .lightbox (99999) dan BUKAN nilai kecil: iframe
   navbar (#curved-navbar-frame) duduk di z-index 1000 dengan hit-area
   82×82px di pojok kanan atas — persis di tempat tombol close modal
   ini. Di bawah 1000, hamburger itu menutupi tombol close-nya. */
.bg-modal-root { position: fixed; inset: 0; z-index: 99999; }

/* fixed inset-0 backdrop-blur-lg overflow-hidden */
.bg-modal {
  position: fixed;
  inset: 0;
  width: 100%;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
  background: rgba(249, 250, 251, 0.72);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  z-index: 10;
}
@media (min-width: 640px) { .bg-modal { border-radius: 0.5rem; } }
@media (min-width: 768px) { .bg-modal { border-radius: 0.75rem; } }

.bg-modal-body { height: 100%; display: flex; flex-direction: column; }
/* flex-1 p-2 sm:p-3 md:p-4 flex items-center justify-center bg-gray-50/50 */
.bg-modal-stage {
  flex: 1 1 0%;
  padding: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(249, 250, 251, 0.5);
}
@media (min-width: 640px) { .bg-modal-stage { padding: 0.75rem; } }
@media (min-width: 768px) { .bg-modal-stage { padding: 1rem; } }

.bg-modal-frame { width: 100%; display: flex; align-items: center; justify-content: center; }

/* relative w-full aspect-[16/9] max-w-[95%] sm:max-w-[85%] md:max-w-3xl
   max-h-[70vh] rounded-lg overflow-hidden shadow-md */
.bg-modal-card {
  position: relative;
  width: 100%;
  max-width: 95%;
  aspect-ratio: 16 / 9;
  max-height: 70vh;
  border-radius: 0.5rem;
  overflow: hidden;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
  cursor: pointer;
}
@media (min-width: 640px) { .bg-modal-card { max-width: 85%; } }
@media (min-width: 768px) { .bg-modal-card { max-width: 48rem; } }

/* background dibuat transparan (dulu rgba(17,24,39,.2)): elemen ini
   menutupi seluruh kotak 16/9, jadi warna apa pun di sini akan
   menimpa .bg-fill di belakangnya dan blur-nya tidak kelihatan. */
.bg-modal-media {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: transparent;
}

/* absolute bottom-0 p-2 sm:p-3 md:p-4 bg-gradient-to-t from-black/50 */
.bg-modal-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.5rem;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
  pointer-events: none;
}
@media (min-width: 640px) { .bg-modal-caption { padding: 0.75rem; } }
@media (min-width: 768px) { .bg-modal-caption { padding: 1rem; } }

.bg-modal-title {
  margin: 0;
  color: #fff;
  font-family: "Plus Jakarta Sans", "DM Sans", system-ui, sans-serif;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
}
.bg-modal-desc {
  margin: 0.25rem 0 0;
  color: rgba(255, 255, 255, 0.8);
  font-size: 0.75rem;
  line-height: 1.4;
}
.bg-modal-meta {
  margin: 0.25rem 0 0;
  color: rgba(255, 255, 255, 0.6);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
@media (min-width: 640px) {
  .bg-modal-title { font-size: 1.125rem; }
  .bg-modal-desc  { font-size: 0.875rem; }
}
@media (min-width: 768px) { .bg-modal-title { font-size: 1.25rem; } }

/* absolute top-3 right-3 p-2 rounded-full bg-gray-200/80 text-gray-700 */
.bg-modal-close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem;
  border: none;
  border-radius: 9999px;
  background: rgba(229, 231, 235, 0.8);
  color: #374151;
  cursor: pointer;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  transition: background 0.18s ease, transform 0.18s var(--ease-soft);
}
.bg-modal-close svg { width: 0.75rem; height: 0.75rem; display: block; }
.bg-modal-close:hover { background: rgba(209, 213, 219, 0.8); transform: scale(1.1); }
.bg-modal-close:active { transform: scale(0.9); }
@media (min-width: 640px) { .bg-modal-close { top: 0.625rem; right: 0.625rem; } }
@media (min-width: 768px) { .bg-modal-close { top: 0.75rem; right: 0.75rem; } }

/* ── DOCK ── fixed z-50 left-1/2 bottom-4 -translate-x-1/2 touch-none */
.bg-dock {
  position: fixed;
  left: 50%;
  bottom: 1rem;
  z-index: 50;
  touch-action: none;
  will-change: transform;
}
/* rounded-xl bg-sky-400/20 backdrop-blur-xl border border-blue-400/30 shadow-lg */
.bg-dock-inner {
  position: relative;
  border-radius: 0.75rem;
  background: rgba(56, 189, 248, 0.2);
  border: 1px solid rgba(96, 165, 250, 0.3);
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  cursor: grab;
}
.bg-dock.is-dragging .bg-dock-inner { cursor: grabbing; }

/* flex items-center -space-x-2 px-3 py-2 */
.bg-dock-row { display: flex; align-items: center; padding: 0.5rem 0.75rem; }
.bg-dock-row > * + * { margin-left: -0.5rem; }

.bg-dock-thumb {
  position: relative;
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
  border-radius: 0.5rem;
  overflow: hidden;
  cursor: pointer;
  will-change: transform;
}
.bg-dock-thumb:hover { z-index: 20; }
.bg-dock-thumb.is-active { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7), 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
@media (hover: hover) and (pointer: fine) {
  .bg-dock-thumb:not(.is-active):hover { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); }
}
@media (min-width: 640px) { .bg-dock-thumb { width: 2.25rem; height: 2.25rem; } }
@media (min-width: 768px) { .bg-dock-thumb { width: 2.5rem; height: 2.5rem; } }

.bg-dock-thumb-media { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
/* bg-gradient-to-b from-transparent via-white/5 to-white/20 */
.bg-dock-sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0.2));
  pointer-events: none;
}
/* absolute -inset-2 bg-white/20 blur-xl — hanya pada thumb aktif */
.bg-dock-glow {
  position: absolute;
  inset: -0.5rem;
  background: rgba(255, 255, 255, 0.2);
  filter: blur(24px);
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
}
.bg-dock-thumb.is-active .bg-dock-glow { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .bento-gallery .bg-card-media,
  .bento-gallery .bg-card-overlay,
  .bg-modal-close { transition: none; }
  .bg-media-spinner,
  .bento-gallery .bg-card--skeleton { animation: none; }
}

/* ── Bento gallery DI ATAS BOARD ──
   Activity gallery memakai permukaan board yang sama persis dengan
   papan sertifikat (.board-wrap > .board — neumorphic surface,
   max-width 1280px). Yang perlu disesuaikan cuma ini: begitu galeri
   jadi anak langsung .board, lebar & padding-nya diserahkan ke board,
   sama seperti .pin-grid — kalau tidak, padding-nya dobel dan grid-nya
   jadi pulau sempit di tengah board yang lebar. */
.board > .bento-gallery {
  max-width: none;
  padding: 0;
}
.board > .bento-gallery .bg-head { margin-bottom: 1.5rem; }

/* Board sertifikat memakai padding-bottom 80px bawaan .board-wrap
   karena dia blok terakhir di section ini. Board activity berhenti
   lebih rapat supaya jarak antar dua board tidak menganga. */
.board-wrap--activity { padding-bottom: 28px; }

/* Tinggi baris grid naik mengikuti lebar board.
   Komponen aslinya memakai auto-rows-[60px] pada container max-w-4xl
   (896px → kolom ±215px), jadi kartu "tall" keluar nyaris persegi.
   Board di sini jauh lebih lebar (konten ±1080px di layar besar), dan
   kalau barisnya tetap 60px kolom yang melebar bikin tiap kartu jadi
   gepeng/letterbox. Angka di bawah menaikkan tinggi baris seiring
   lebar kolom supaya rasio kartunya tetap sedekat mungkin dengan
   desain aslinya. Di bawah 768px grid belum 4 kolom, jadi 60px asli
   dipertahankan apa adanya. */
@media (min-width: 768px) {
  .board > .bento-gallery .bg-grid { grid-auto-rows: 56px; }
}
@media (min-width: 1024px) {
  .board > .bento-gallery .bg-grid { grid-auto-rows: 68px; }
}
@media (min-width: 1280px) {
  .board > .bento-gallery .bg-grid { grid-auto-rows: 76px; }
}

/* ════════════════════════════════════════════════════════════════
   PERMUKAAN BOARD ACHIEVEMENTS — tekstur kertas prosedural.

     board activity    → kertas kraft, krem hangat berbintik
     board certificate → kertas putih keabuan

   Dua-duanya kertas polos. Board sertifikat dulu punya pola garis
   horizontal di atas kertasnya; pola itu sudah dihapus sepenuhnya,
   sekarang yang membedakan dua board cuma warna kertasnya.

   Grain-nya DIBANGKITKAN, bukan foto: dua layer SVG feTurbulence yang
   ditanam sebagai data-URI di dalam CSS. Alasannya nol request
   jaringan, nol file aset yang harus ikut ke repo, tetap tajam di
   devicePixelRatio berapa pun, dan warnanya bisa di-tune lewat token
   --paper-* tanpa menyentuh gambar apa pun.

   Dua layer, karena kertas sungguhan punya dua skala tekstur:
     · grain halus  (baseFrequency tinggi)  → serat/bintik rapat
     · mottle kasar (baseFrequency rendah)  → belang awan, ketidak-
                                              rataan warna antar area
   Satu layer saja terbaca seperti noise TV, bukan kertas.

   Keduanya di-blend `multiply` di atas --paper, jadi grain hanya
   MENGGELAPKAN warna dasar — nilainya jadi tidak perlu dihitung
   ulang tiap kali warna kertasnya diganti.
   ════════════════════════════════════════════════════════════════ */
.board-wrap--activity .board,
.board-wrap--certificate .board {
  /* '#' di dalam data-URI WAJIB ditulis %23 — kalau tidak, browser
     memotong URI-nya di situ dan filter-nya tidak pernah ketemu. */
  --grain-fine: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='f' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23f)' opacity='0.30'/%3E%3C/svg%3E");
  --grain-mottle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='420' height='420'%3E%3Cfilter id='m' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.014' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='420' height='420' filter='url(%23m)' opacity='0.38'/%3E%3C/svg%3E");

  background-color: var(--paper);
  /* stitchTiles='stitch' di atas yang bikin ubinan grain-nya nyambung;
     ukuran ubin harus sama dengan width/height SVG-nya, kalau tidak
     sambungannya kelihatan sebagai kotak-kotak berulang. */
  background-size: 160px 160px, 420px 420px;
  box-shadow: 8px 8px 18px var(--paper-shade), -8px -8px 18px var(--paper-glow);
}

/* ── Kertas kraft (gambar referensi 1) ── */
.board-wrap--activity .board {
  --paper:       #ece0c2;
  --paper-shade: #cdbf9c;
  --paper-glow:  #fdf8e9;

  background-image: var(--grain-fine), var(--grain-mottle);
  background-blend-mode: multiply, multiply;
}

/* ── Kertas putih keabuan (gambar referensi 2) ──
   Permukaannya murni tekstur kertas — perlakuan yang sama persis
   dengan board activity, cuma beda warna kertas. Jadi layer-nya juga
   sama: dua grain, dua blend mode, dan background-size-nya cukup
   diwarisi dari blok bersama di atas. */
.board-wrap--certificate .board {
  --paper:       #f3f2ee;
  --paper-shade: #d6d4cd;
  --paper-glow:  #ffffff;

  background-image: var(--grain-fine), var(--grain-mottle);
  background-blend-mode: multiply, multiply;
}


/* CATATAN kepala board sertifikat (.board-head): stylingnya TIDAK di
   sini, melainkan digabung dengan kepala galeri activity di blok
   "KEPALA GALERI" di bawah — satu definisi untuk dua board.

   Dulu di sini ada plate --paper solid, karena judulnya harus terbaca
   di atas pola garis. Polanya sudah dihapus, jadi plate itu bukan cuma
   tidak perlu lagi — dia malah merusak: bidang warna rata di atas
   permukaan bergrain akan tampak sebagai tambalan mulus yang
   kelihatan jelas. */

/* ════════════════════════════════════════════════════════════════
   TIPOGRAFI SECTION ACHIEVEMENTS

   Tiga tingkat, semuanya memakai keluarga font yang MEMANG SUDAH
   dimuat di <link> Google Fonts halaman ini — tidak ada request baru:

     display  → Playfair Display   judul besar
     data     → JetBrains Mono     eyebrow, counter, tanggal
     UI/teks  → DM Sans            subjudul, plate, badge

   Playfair bukan font asing di sini: dia sudah jadi wajah display
   situs ini (.lb-story-title, .fan-info-title). Serif juga memang
   lebih pas untuk halaman kredensial daripada grotesque — terbaca
   seperti ijazah/piagam, bukan seperti dashboard.

   ── KENAPA SEMUA font-family DI BLOK INI PAKAI !important ──
   Di L2517 ada aturan global `body, * { font-family: 'DM Sans', …
   !important }`. Karena !important, dia mengalahkan SEMUA deklarasi
   font-family biasa berapa pun spesifisitasnya — termasuk selektor
   ber-ID seperti #certificate .bg-heading. Jadi tanpa !important di
   sini, seluruh blok tipografi ini tidak berefek sama sekali dan
   section-nya tetap ter-render DM Sans. Ini pola yang sudah dipakai
   semua override font lain di file ini (L2518-L2768, L3338, L3826).
   ════════════════════════════════════════════════════════════════ */

/* ── Display ── */
#certificate .page-title,
#works .page-title,
#about .page-title,
#certificate .board-heading,
#certificate .bg-heading {
  font-family: 'Playfair Display', Georgia, serif !important;
  font-weight: 700 !important;
  /* -0.04em bawaan .page-title ditrack untuk grotesque; di serif,
     serapat itu bikin serif antar huruf saling tabrak */
  letter-spacing: -0.015em !important;
}

#certificate .page-title {
  font-size: clamp(34px, 4.4vw, 60px);
  line-height: 1.05;
}

#works .page-title {
  font-size: clamp(28px, 4vw, 46px) !important;
  line-height: 1.1 !important;
}

#about .page-title {
  font-size: clamp(30px, 4.2vw, 48px) !important;
  line-height: 1.1 !important;
}

#certificate .board-heading {
  margin: 0;
  font-size: clamp(26px, 3vw, 40px);
  line-height: 1.15;
  color: var(--text-primary);
}

/* Judul modal galeri hidup di luar #certificate (di-append ke <body>),
   jadi tidak kena scope di atas dan harus disebut sendiri. */
.bg-modal-title {
  font-family: 'Playfair Display', Georgia, serif !important;
  font-weight: 700 !important;
  letter-spacing: -0.01em !important;
}

/* ── Data ──
   Mono sudah jadi "wajah data" di situs ini (.cert-count, .cert-date
   memang sudah JetBrains Mono); eyebrow ikut disamakan supaya label
   kecil di section ini konsisten satu keluarga. */
#certificate .page-eyebrow,
#works .page-eyebrow,
#about .page-eyebrow,
#certificate .bg-eyebrow,
#certificate .board-eyebrow {
  font-family: 'JetBrains Mono', ui-monospace, monospace !important;
  font-weight: 700 !important;
  letter-spacing: 0.2em !important;
}

/* Nama penerima di kertas sertifikat: di piagam sungguhan nama
   penerima memang di-set serif. */
#certificate .cert-recipient {
  font-family: 'Playfair Display', Georgia, serif !important;
  font-weight: 700 !important;
  letter-spacing: 0 !important;
}

/* ════════════════════════════════════════════════════════════════
   HEADER SECTION — judul di tengah atas (HANYA MOBILE/TABLET)
   Header Works & Achievements dulu disembunyikan global (lihat L4192,
   dianggap redundan dengan nav aktif). Kini dipakai lagi sebagai judul
   section: rata tengah di atas page, memakai font display yang sama
   (Playfair Display serif) seperti bagian Achievements — Works dan
   About Me ikut memakai wajah itu, ukurannya disesuaikan per section.
   "CREDENTIALS" (eyebrow) & chip "Total: N certificates" dihapus.
   Di desktop (≥768px) header tetap disembunyikan seperti semula. */
@media (max-width: 767px) {
  #works .page-header,
  #certificate .page-header,
  #about .page-header {
    display: flex !important;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    max-width: 1280px;
  }
  #works > hr.divider,
  #certificate > hr.divider,
  #about > hr.divider {
    display: block !important;
  }
  #works .page-eyebrow,
  #about .page-eyebrow,
  #certificate .page-eyebrow {
    color: var(--blue);
    margin-bottom: 10px;
  }
}
@media (min-width: 768px) {
  #works .page-header,
  #certificate .page-header,
  #about .page-header,
  #works > hr.divider,
  #certificate > hr.divider,
  #about > hr.divider {
    display: none !important;
  }
}

/* ════════════════════════════════════════════════════════════════
   KEPALA GALERI — hubungan judul ↔ grid

   Sebelumnya: judul raksasa + subjudul melayang di tengah board,
   lalu jarak kosong lebar, baru grid. Judul dan grid terbaca sebagai
   dua benda terpisah yang kebetulan setumpuk.

   Sekarang disusun sebagai satu kepala: eyebrow mono kecil → judul
   serif → subjudul → garis rambut selebar konten. Garis itu yang
   mengikat kepala ke grid, dan ukuran judul diturunkan supaya tidak
   lagi mendominasi board.
   ════════════════════════════════════════════════════════════════ */
/* Selektornya sengaja digabung galeri activity (.bg-*) + board
   sertifikat (.board-*): dua kepala board di section ini harus benar-
   benar SATU perlakuan, bukan dua definisi mirip yang lama-lama
   melenceng sendiri saat salah satunya disetel. */
.bento-gallery .bg-head,
.board-wrap--certificate .board-head {
  margin-bottom: 22px;
  padding-bottom: 18px;
  border-bottom: 1px solid rgba(58, 51, 39, 0.16);
  text-align: center;
}

.bento-gallery .bg-eyebrow,
.board-wrap--certificate .board-eyebrow {
  margin: 0 0 10px;
  font-size: 10.5px;
  text-transform: uppercase;
  color: #8a8172;
}

/* Judul: ukuran diturunkan dari clamp(1.5rem…2.25rem) supaya
   proporsinya seimbang dengan grid di bawahnya, dan gradient-text
   bawaan komponen dilepas — di atas board krem, teks solid jauh
   lebih tegas daripada gradient abu yang nyaris rata. */
.bento-gallery .bg-heading {
  font-size: clamp(26px, 3vw, 40px) !important;
  line-height: 1.15;
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: var(--text-primary);
}

.bento-gallery .bg-sub,
.board-wrap--certificate .board-sub {
  margin: 8px auto 0;
  max-width: 46ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: #6f6759;
}

/* ════════════════════════════════════════════════════════════════
   COMPREHENSIVE MOBILE RESPONSIVE
   ════════════════════════════════════════════════════════════════ */

/* ── TABLET (≤ 860px) ── */
@media (max-width: 860px) {
  .page-header {
    padding: 40px 4vw 24px;
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .page-title { font-size: clamp(22px, 5vw, 36px) !important; }
  .board-wrap { padding: 12px 4vw 60px; }
  .board { padding: 28px 20px 24px; }
  .wrap, .wrap-about { padding: 48px 4vw 80px; }
  .connect-panel { gap: 24px; }
  .connect-form { padding: 20px; border-radius: 18px; }
  .connect-list { grid-template-columns: 1fr; }
  .connect-item { padding: 14px 16px; }
  .modal { width: 96%; border-radius: 20px; }
  .modal-slideshow { height: 280px; }
  .modal-body { padding: 24px; }
  .modal-title { font-size: 24px; }
  .lb-story-scroll { column-gap: 10px; }
}

/* ── MOBILE (≤ 640px) ── */
@media (max-width: 640px) {
  /* Hero */
  .hero {
    padding-top: 24px;
    min-height: auto;
    gap: 24px;
  }
  .hero-heading {
    font-size: clamp(28px, 8vw, 40px) !important;
    line-height: 1.1 !important;
    margin-bottom: 16px;
  }
  .hero-sub {
    font-size: 14px;
    max-width: 100%;
    margin-bottom: 28px;
  }
  .terminal-box {
    max-width: 100%;
    border-radius: 10px;
  }
  .terminal-body {
    padding: 14px 16px;
    font-size: 12px;
    min-height: 60px;
  }
  .hero-socials {
    gap: 10px;
    margin-top: 32px;
  }
  .hero-socials a {
    width: 40px;
    height: 40px;
  }
  .skill-pills {
    margin-bottom: 20px;
  }
  .pill-typing {
    font-size: 10px;
    padding: 5px 12px;
  }

  /* Page header */
  .page-header {
    padding: 32px 4vw 16px;
  }
  .page-eyebrow { font-size: 9px !important; }
  .page-title { font-size: clamp(20px, 6vw, 32px) !important; }
  .page-sub { font-size: 13px; margin-top: 10px; }
  .divider { margin: 0 4vw; }

  /* About */
  .wrap, .wrap-about { padding: 36px 4vw 60px; }
  .intro-name {
    font-size: clamp(28px, 8vw, 48px) !important;
    margin-bottom: 16px;
  }
  .intro-bio {
    font-size: 14px;
    line-height: 1.7;
  }
  .intro-photo-wrap {
    width: clamp(160px, 50vw, 240px);
  }
  .sec { margin-bottom: 40px; }
  .sec-label { font-size: 10px; margin-bottom: 16px; }

  /* Facts */
  .fact {
    padding: 14px 16px;
    gap: 12px;
  }
  .fact-key { font-size: 10px; }
  .fact-val { font-size: 13px; }

  /* Connect */
  .connect-title {
    font-size: clamp(22px, 6vw, 32px);
  }
  .connect-desc {
    font-size: 14px;
    margin-bottom: 20px;
  }
  .connect-socials a { width: 38px; height: 38px; }
  .connect-form { padding: 16px; border-radius: 14px; }
  .cf-field input, .cf-field textarea {
    padding: 10px 12px;
    font-size: 13px;
  }
  .cf-submit { padding: 12px 18px; font-size: 13px; }
  .cv-btn { padding: 10px 16px; font-size: 12px; }

  /* Board */
  .board-wrap { padding: 8px 4vw 48px; }
  .board { padding: 20px 16px 20px; border-radius: 16px; }
  .board-head { margin-bottom: 16px; padding-bottom: 14px; }
  .board-heading { font-size: clamp(20px, 5vw, 28px); }

  /* Modal */
  .modal { width: 98%; max-width: none; border-radius: 16px; }
  .modal-slideshow { height: 220px; border-radius: 16px 16px 0 0; }
  .modal-body { padding: 20px; }
  .modal-title { font-size: 22px; margin-bottom: 8px; }
  .modal-desc { font-size: 14px; margin-bottom: 16px; }
  .modal-meta-row { gap: 10px; }
  .modal-meta-label { font-size: 10px; min-width: 70px; }

  /* Lightbox */
  .lightbox { padding: 16px 8px; }
  .lightbox-inner {
    border-radius: 16px;
    max-height: calc(100vh - 32px);
  }
  .lightbox-close {
    top: 20px !important;
    right: 16px !important;
    width: 34px;
    height: 34px;
    font-size: 15px;
  }
  .lb-story-head {
    padding: 20px 16px;
    border-radius: 14px 14px 0 0;
  }
  .lb-story-title { font-size: clamp(20px, 5vw, 28px); }
  .lb-story-scroll {
    padding: 24px 12px 0;
    border-radius: 0 0 14px 14px;
  }
  .lb-story-gallery {
    padding: 16px 12px;
    gap: 10px;
    border-radius: 0 0 14px 14px;
  }
  .lb-story-gallery-img { border-radius: 10px; }
  .lb-body { padding: 20px; }
  .lb-title { font-size: 20px; }
  .lb-desc { font-size: 14px; }

  /* Certificate grid */
  .pin-card .frame-wrap { transform: none !important; }
  .cert-paper { padding: 6px; border-radius: 8px; }
  .cert-body { padding: 16px 20px; }
  .cert-recipient { font-size: 16px; }
  .cert-course { font-size: 13px; }
  .cert-photo-footer { padding: 14px 16px; }
}

/* ── SMALL MOBILE (≤ 480px) ── */
@media (max-width: 480px) {
  .hero-heading {
    font-size: clamp(24px, 7vw, 34px) !important;
  }
  .terminal-body {
    padding: 12px 14px;
    font-size: 11px;
  }
  .terminal-bar { padding: 10px 14px; }
  .terminal-title { font-size: 10px; }
  .intro-name {
    font-size: clamp(24px, 7vw, 36px) !important;
  }
  .connect-title {
    font-size: clamp(20px, 6vw, 28px);
  }
  .connect-actions { flex-direction: column; align-items: stretch; }
  .cv-btn, .cf-submit { justify-content: center; }
  .modal-meta-row { flex-direction: column; gap: 8px; }
  .modal-meta-label { min-width: auto; }
  .cert-badge { font-size: 10px; padding: 3px 8px; }
  .frame-plate { padding: 5px 10px; }
  .frame-plate-title { font-size: 10px; }
  .frame-plate-meta { font-size: 8.5px; }
}

/* ── TINY MOBILE (≤ 360px) ── */
@media (max-width: 360px) {
  .hero-heading {
    font-size: 22px !important;
  }
  .terminal-body {
    padding: 10px 12px;
    font-size: 10px;
    min-height: 50px;
  }
  .hero-socials a {
    width: 36px;
    height: 36px;
  }
  .hero-socials a svg {
    width: 14px;
    height: 14px;
  }
  .intro-name {
    font-size: 22px !important;
  }
  .connect-title {
    font-size: 18px;
  }
  .lb-story-head {
    padding: 16px 12px;
  }
  .lb-story-title {
    font-size: 18px;
  }
}

/* ════════════════════════════════════════════════════════════════
   MOBILE REDESIGN (≤ 640px) — full mobile pass.
   Di desktop setiap section adalah layar penuh / kamera cinematic
   (split-screen, wheel-jack, bento interaktif, papan pin miring).
   Di bawah 640px semua itu runtuh jadi tumpukan kartu biasa yang jauh
   lebih kecil dari konsep aslinya. Blok ini memulihkan IDENTITAS visual
   desktop ke mobile: hero foto dikembalikan, Works jadi urutan kartu
   satu-persatu yang tetap berwarna per item, bento dirapikan, papan pin
   dikasih karakter, dan About/Connect ditata ulang — semua tanpa
   sentuhan JS (murni override CSS di akhir file agar menang).
   ════════════════════════════════════════════════════════════════ */
@media (max-width: 640px) {

  /* ── 1. HERO ──
     Desktop menampilkan foto besar + badge melayang di kanan.
     Mobile menaruhnya DI BAWAH teks sebagai "card hero" penuh lebar,
     dengan badge yang tersusun rapi (tidak menumpuk di tepi). */
  .hero {
    /* beri sedikit ruang ekstra setelah foto, bertahap antar blok */
    gap: 34px;
  }
  /* foto yang tadi disembunyikan di ≤1024px dikembalikan,
     disusun sebagai kolom penuh di bawah teks hero */
  .hero-right {
    display: flex !important;
    flex: none;
    width: 100%;
    order: 3;
    margin-top: 8px;
  }
  .hero-photo-wrap {
    width: 100%;
    /* foto satu item pada barisnya sendiri, pill badge mengalir
       (flex-wrap) di baris berikutnya — bukan menumpuk satu per baris */
    flex-direction: row;
    flex-wrap: wrap;
    row-gap: 18px;
  }
  .hero-left {
    width: 100%;
  }
  .hero-photo-card {
    width: min(72vw, 300px);
    height: auto;
    aspect-ratio: 3/4;
    margin: 0 auto;
    border-radius: 38px;
    box-shadow:
      12px 12px 32px #bebebe,
      -12px -12px 32px #ffffff;
  }
  .hero-card-img {
    aspect-ratio: 3/4;
    border-radius: 26px;
  }
  /* aura terlalu besar untuk layar sempit — kecilkan saja; posisinya
     tetap terpusat lewat static-position flex (pola yang sama dengan
     base desktop), jangan pakai left/top+translate karena keyframe
     auraPulse memakai transform:scale dan akan menimpa offset itu */
  .hero-photo-wrap::before {
    width: 220px;
    height: 220px;
  }
  /* badge jadi pill terpisah, sejajar di bawah foto */
  .hero-photo-wrap .fb-parallax,
  .hero-photo-wrap .fbadge {
    position: static !important;
    display: inline-flex;
    margin: 0;
  }
  .fb-parallax {
    inset: auto;
  }
  .hero-photo-wrap .fb-parallax {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    flex: 0 0 auto;
    margin-top: 0;
  }
  .fbadge {
    white-space: nowrap;
  }

  /*── terminal mengambang penuh lebar biar terbaca di layar sempit --*/
  .terminal-box {
    width: 100%;
    max-width: none;
    margin-left: 0 !important;
    margin-right: 0 !important;
  }
  .terminal-body {
    font-size: 12.5px;
  }
  /* sosial dipindah JS ke bawah foto (lihat premium.js): pastikan order
     di belakang foto dan beri jarak + pusatkan ikon */
  .hero-socials {
    order: 4;
    margin-top: 34px;
    justify-content: center;
  }

  /* ── 2. WORKS — pengganti tampilan "split-screen" jadi kartu
     setiap project tetap membawa warnanya sendiri (palet section dari
     works-splitscroll.js), dibungkus rapi sebagai kartu terpisah agar
     satu project dan satu projectnya jelas terbaca. Layout foto-atas /
     teks-bawah, foto dibuat lebih berbobot. ── */

  /* FIX kunci: #works harus tetap bisa discroll di dalam kartu. Rule
     lama (L4210) men-set overflow:visible — konten yang memanjang
     jadi bocor keluar kartu dan tidak terjangkau di layar sempit.
     Format ulang jadi scroll container normal dengan clearance
     kecil di atas, bukan 108px (navbar sekarang iframe, tidak
     menutupi kartu). */
  #works {
    height: auto !important;
    max-height: none !important;
    overflow-y: auto !important;
    overflow-x: hidden !important;
    padding-top: 10px !important;
  }
  .split-page {
    margin-bottom: 26px;
    border-radius: 20px;
    overflow: hidden;
    background: #fff;
    box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
    border: 1px solid rgba(15, 23, 42, 0.05);
  }
  .split-half {
    width: 100%;
  }
  .split-image {
    min-height: 230px !important;
    width: 100%;
    height: auto;
  }
  .split-photo {
    width: 100% !important;
    height: auto !important;
  }
  .split-text {
    position: relative;
    z-index: 1;
  }
  .flow-eyebrow { font-size: 11px; letter-spacing: .22em; }
  .flow-title {
    font-size: clamp(1.5rem, 8vw, 2.4rem);
    line-height: 1.08;
    text-transform: uppercase;
  }
  .flow-p {
    font-size: 0.98rem;
    line-height: 1.6;
    max-width: 60ch;
  }
  .flow-stack { margin-top: 2px; }
  .flow-meta { margin-top: 2px; }
  /* urutkan foto lebih dulu, teks menyusul, untuk setiap paruh */
  .split-half--left, .split-half--right { order: 2; }
  .split-half:has(.split-image) { order: 1; }

  /* ── 3. ACHIEVEMENTS — bento gallery ── */
  .bento-gallery {
    padding: 1.25rem 0 0.25rem;
  }
  .bento-gallery .bg-head {
    margin-bottom: 1.25rem;
  }
  .bento-gallery .bg-heading { font-size: 1.4rem; line-height: 1.25; }
  .bento-gallery .bg-sub { font-size: 0.85rem; }
  .bento-gallery .bg-grid {
    /* baris per item dibuat lebih tinggi di mobile supaya kartu
       cukup besar untuk tap — bukan grid otomatis sempit */
    grid-auto-rows: 64px;
    gap: 10px;
  }
  /* Bentuk kartu mengikuti rasio asli foto di mobile juga:
     potret paling tinggi, lanskap paling rendah — seperti bento
     di desktop tapi ditumpuk satu kolom. */
  .bento-gallery .bg-card { grid-row: span 4 / span 4; }
  .bento-gallery .bg-span-portrait  { grid-row: span 6 / span 6; }
  .bento-gallery .bg-span-square    { grid-row: span 5 / span 5; }
  .bento-gallery .bg-span-landscape { grid-row: span 4 / span 4; }

/* ── 4. CERTIFICATES — papan pin ──
     Dulu 1 kolom: tiap sertifikat "Certificate of Completion" yang
     ditata untuk kertas lanskap dipaksa wrap ke kolom 124px → kartu jadi
     tinggi jendul (±335px) dan menjuntai panjang ke bawah. Di mobile
     dokumennya DIPERKECIL ENTRE proporsional (lebar 200% + scale 0.5,
     jadi teks tidak re-wrap) demi menempatkan tiap kartu sebagai tile
     kompak 2 kolom sejajar bento/works — detail penuh tetap di lightbox. */
  .pin-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 10px;
    padding-top: 4px;
  }
  .pin-card {
    width: 100%;
    margin-bottom: 0;
  }
  .pin-card .frame-wrap {
    transform: rotate(0deg) translate(0, 0) !important;
  }
  .cert-paper {
    padding: 3px;
    border-radius: 8px;
    aspect-ratio: 4 / 3;
    overflow: hidden;
  }
  .cert-paper::before,
  .cert-paper::after { display: none; }
  .cert {
    width: 200%;
    transform: scale(0.5);
    transform-origin: top left;
    border-radius: 4px;
  }
  .cert-banner { height: 5px; }
  .cert-body { padding: 10px 12px; }
  .cert-platform { font-size: 8.5px; letter-spacing: .1em; margin-bottom: 5px; }
  .cert-recipient { font-size: 12px; margin-bottom: 5px; }
  .cert-course { font-size: 10.5px; line-height: 1.35; margin-bottom: 8px; }
  .cert-divider { margin: 8px 0; }
  .cert-date { font-size: 9px; }
  .cert-badge { font-size: 8.5px; padding: 2px 6px; }
  .cert-seal { display: none; }
  .cert-photo { max-height: 110px; }
  .cert-photo-footer { padding: 8px 10px; gap: 6px; flex-wrap: wrap; }
  .cert-photo-name { font-size: 9.5px; }
  .cert-photo-platform { font-size: 8px; }
  .cert-photo-badges { gap: 3px; flex-direction: row; align-items: center; }
  .frame-plate {
    margin: 6px auto 0;
    max-width: 96%;
    padding: 3px 6px;
    border-radius: 6px;
  }
  .frame-plate-title { font-size: 8.5px; }
  .frame-plate-meta { font-size: 7px; margin-top: 1px; }

  /* ── 5. ABOUT ── */
  .intro {
    gap: 28px;
    text-align: center;
  }
  .intro-name { line-height: 1.06; }
  .intro-photo-wrap {
    width: min(62vw, 240px);
    border-radius: 24px;
  }
  .intro-photo { border-radius: 16px; }
  .facts {
    grid-template-columns: 1fr !important;
    gap: 12px !important;
  }
  .fact { text-align: left; }

  /* ── 6. LET'S CONNECT ── */
  .connect-panel { gap: 26px; }
  .connect-title { line-height: 1.18; }
  .connect-actions {
    justify-content: center;
    text-align: center;
  }
  .connect-form {
    padding: 18px;
    border-radius: 16px;
  }

  /* ── 7. ACHIEVEMENTS — kepala section (label + pembatasnya sengaja
     disembunyikan global di L4192; di mobile justru dibutuhkan supaya
     section pertama-tama terbaca "Achievements", bukan papan kertas
     tanpa judul). ── */
  #certificate .page-header,
  #works .page-header,
  #about .page-header {
    display: flex !important;
    padding: 2px 0 0;
  }
  #certificate > hr.divider,
  #works > hr.divider,
  #about > hr.divider {
    display: block !important;
    margin: 16px 0 6px;
  }
  #certificate .page-title { font-size: clamp(22px, 7vw, 34px) !important; }
  #works .page-title { font-size: clamp(20px, 6.5vw, 32px) !important; line-height: 1.15 !important; }
  #about .page-title { font-size: clamp(20px, 6.5vw, 32px) !important; }

  /* ── 7b. bento — kartu tidak boleh jadi raksasa setinggi separuh
     layar di kolom tunggal; baris grid sedikit lebih rendah, judul
     kartu lebih terbaca, biar komposisi awal langsung "enak
     dibaca". ── */
  .bento-gallery .bg-grid {
    grid-auto-rows: 60px;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }
  .bento-gallery .bg-card { grid-column: span 1 / span 1; grid-row: span 3 / span 3; }
  .bento-gallery .bg-span-portrait  { grid-column: span 1 / span 1; grid-row: span 4 / span 4; }
  .bento-gallery .bg-span-square    { grid-column: span 1 / span 1; grid-row: span 3 / span 3; }
  .bento-gallery .bg-span-landscape { grid-column: span 2 / span 2; grid-row: span 3 / span 3; }
  .bento-gallery .bg-card-overlay { padding: 0.65rem; }
  .bento-gallery .bg-card-title { font-size: 0.9rem; }
  .bento-gallery .bg-card-desc  { font-size: 0.72rem; }

  /* ── 8. POPUP SCROLL-STORY (#act-lightbox) — kolapse lama L1730
     menghilangkan FOTO sama sekali (`.lb-story-media{display:none}`)
     + crossfade scroll-linked dimatikan JS di mobile → popup tampil
     hanya teks kosong dan terasa "aneh". Dipulihkan jadi cerita
     1 kolom: foto sticky di atas, teks beat discroll, crossfade tetap
     jalan (works-scrollstory.js sudah disesuaikan di layout/apply).
     `position:sticky` membutuhkan media TIDAK display:none. ── */
  #act-lightbox .lightbox-inner { height: min(760px, calc(100vh - 80px)); }
  .lb-story-scroll { padding: 14px 14px 0; }
  .lb-story-media {
    display: block;
    position: sticky;
    top: 12px;
    z-index: 2;
    justify-self: auto;
    width: 100%;
    max-width: none;
    height: clamp(220px, 46vh, 340px);
    margin: 0 0 8px;
  }
  .lb-story-scroll--single .lb-story-media {
    position: static;
    max-width: min(86%, 400px);
    height: clamp(200px, 42vh, 320px);
    margin: 12px auto 0;
  }
  .lb-story-beats { padding: 0 2px 44px; gap: 20vh; }
  /* galeri foto tanpa judul/deskripsi (popup Works): kolapse lama L1746
     menjadikannya 1 kolom penuh layar → tiap foto "raksasa". Di mobile
     dibuat tetap 2 kolom berdampingan dengan tile kecil, biar beberapa
     foto terlihat sekaligus tanpa scroll panjang. */
  .lb-story-gallery {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    padding: 14px 14px 44px;
  }

  /* ── 9. MODAL BENTO (bg-modal) — dulu `overflow:hidden` + kartu
     16/9 dengan caption menempel absolut → judul/deskripsi panjang
     terpenggal dan tidak bisa discroll. Di mobile dibuat mengalir:
     media di atas, caption di bawah, panel discroll kalau konten
     melebihi layar; sisakan ruang bawah untuk dock thumbnail. ── */
  .bg-modal { overflow-y: auto; }
  .bg-modal-body { min-height: 100%; }
  .bg-modal-stage { align-items: flex-start; padding: 0.5rem 0.5rem 4.75rem; }
  .bg-modal-frame { align-items: flex-start; }
  .bg-modal-card {
    width: 100%;
    max-width: 100%;
    aspect-ratio: auto;
    max-height: none;
    display: flex;
    flex-direction: column;
  }
  .bg-modal-media {
    flex: 0 0 auto;
    height: min(52vh, 380px);
    aspect-ratio: auto;
  }
  .bg-modal-caption {
    position: static;
    padding: 0.875rem;
    background: linear-gradient(to top, rgba(0, 0, 0, .7), rgba(0, 0, 0, .3) 55%, rgba(0, 0, 0, .18));
  }

  /* ── 19. TIPOGRAFI MOBILE — skala teks ≤640px. Judul display yang
     meniru skala desktop (clamp basis vw) ter-render kegedean di kolom
     390px; semua ditarik ke skala rapat biar tidak "aneh". Diletakkan
     paling akhir di blok mobile supaya menang atas rule sespesifisitas
     di atasnya. ── */
  .flow-title {
    font-size: clamp(1.2rem, 6.2vw, 1.8rem);
    line-height: 1.12;
    letter-spacing: -0.01em;
  }
  .flow-p { font-size: 0.92rem; line-height: 1.58; }
  .flow-meta { font-size: 10.5px; letter-spacing: 0.08em; }
  .flow-stack .lb-stack-tag { font-size: 10.5px; padding: 4px 11px; }
  .flow-badge { font-size: 10.5px; }
  .bento-gallery .bg-heading { font-size: 1.3rem !important; line-height: 1.2 !important; }
  .bento-gallery .bg-sub { font-size: 0.82rem; }
  #certificate .board-heading { font-size: 20px !important; line-height: 1.25 !important; }
  .board-wrap--certificate .board-sub { font-size: 12.5px; }
  .lb-story-caption { font-size: clamp(17px, 4.6vw, 21px); }
  .lb-story-sub { font-size: 14px; }
  .lb-story-tag { font-size: 10.5px; }
}

/* ── 6b. hemat vertikal di layar sedang (640–767px):
     foto hero tetap tampil tapi lebih kecil, bukan disembunyikan ── */
@media (min-width: 641px) and (max-width: 767px) {
  .hero-right {
    display: flex;
    flex: none;
    width: 100%;
  }
  .hero-socials {
    order: 4;
    margin-top: 30px;
    justify-content: center;
  }
  #certificate .page-header,
  #works .page-header,
  #about .page-header {
    display: flex !important;
    padding: 4px 0 0;
  }
  #certificate > hr.divider,
  #works > hr.divider,
  #about > hr.divider {
    display: block !important;
    margin: 16px 0 6px;
  }
  .hero-photo-wrap {
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
    row-gap: 18px;
  }
  .hero-left {
    width: 100%;
  }
  .hero-photo-card {
    width: min(300px, 60vw);
    height: auto;
    aspect-ratio: 3/4;
  }
  .hero-card-img {
    aspect-ratio: 3/4;
  }
  /* badge jadi baris di bawah foto — wrapper parallax yang tadinya
     absolute inset:0 dicabut supaya tidak menumpuk di satu titik */
  .hero-photo-wrap .fb-parallax,
  .hero-photo-wrap .fbadge {
    position: static !important;
  }
  .hero-photo-wrap .fb-parallax {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    flex: 0 0 auto;
    margin-top: 0;
  }
  .hero-photo-wrap .fb-parallax .fbadge {
    position: static !important;
  }
}