/* ==========================================================================
   Systrox Solution — page styles
   --------------------------------------------------------------------------
   Design tokens live in tokens.css and are loaded first. No raw hex or rgba()
   colour literals belong in this file: use a semantic token, or compose an
   alpha from a channel triplet, e.g. rgb(var(--accent-ch) / .18).
   ========================================================================== */

* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  line-height: var(--body-leading);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
img { display: block; max-width: 100%; }

/* ---------- LAYOUT ---------- */
.container {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 32px;
}
@media (max-width: 720px) {
  .container { padding: 0 20px; }
}

section { position: relative; }

/* ---------- NAVBAR ---------- */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  padding: 18px 0;
  transition: background .35s ease, border-color .35s ease, padding .25s ease;
  border-bottom: 1px solid transparent;
  backdrop-filter: blur(0px);
}
.nav.scrolled {
  background: rgb(var(--black-ch) / 0.65);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border-bottom-color: var(--line);
  padding: 12px 0;
}
.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  /* The language switcher competes for nav width on small screens; without
     this the brand column gets squeezed and .brand-sub wraps to two lines. */
  flex: none;
}
.brand-mark {
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: 10px;
  background: linear-gradient(160deg, rgb(var(--neutral-ch) / 0.18), rgb(var(--accent-ch) / 0.10));
  border: 1px solid var(--line-strong);
  overflow: hidden;
}
.brand-mark img { width: 100%; height: 100%; object-fit: cover; }
.brand-name {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' var(--display-width);
  font-weight: 700;
  letter-spacing: 0.18em;
  font-size: 14px;
  color: var(--neutral-100);
}
.brand-sub {
  display: block;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--text-faint);
  letter-spacing: 0.32em;
  margin-top: 2px;
  white-space: nowrap;
}
.nav-links {
  display: flex;
  gap: var(--nav-gap);
  align-items: center;
}
/* Language switcher + CTA travel together as the right-hand cluster. Without
   this they are separate flex children and `space-between` on .nav-inner
   spreads four items across the bar, stranding the switcher mid-gap. */
.nav-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: none;
}
.nav-links a {
  font-size: 13px;
  color: var(--text-dim);
  transition: color .2s ease;
  position: relative;
  letter-spacing: 0.02em;
}
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; bottom: -6px;
  width: 0; height: 1px;
  background: var(--accent);
  transition: width .25s ease;
}
.nav-links a:hover { color: var(--neutral-100); }
.nav-links a:hover::after { width: 100%; }
.nav-cta {
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  font-size: 13px;
  color: var(--neutral-100);
  transition: all .25s ease;
  background: rgb(var(--white-ch) / 0.02);
}
.nav-cta:hover {
  border-color: var(--accent);
  color: var(--neutral-000);
}
@media (max-width: 860px) {
  .nav-links { display: none; }
  .nav-cta { display: none; }
}

/* ---------- MOBILE MENU ---------- */
.nav-hamburger {
  display: none;
  width: 40px; height: 40px;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  background: rgb(var(--white-ch) / 0.02);
  cursor: pointer;
  position: relative;
  z-index: 51;
}
.nav-hamburger span {
  position: absolute;
  left: 12px; right: 12px;
  height: 1.5px;
  background: var(--neutral-100);
  border-radius: 2px;
  transition: transform .35s cubic-bezier(.2,.8,.2,1), opacity .25s ease, top .35s cubic-bezier(.2,.8,.2,1);
}
.nav-hamburger span:nth-child(1) { top: 14px; }
.nav-hamburger span:nth-child(2) { top: 19px; }
.nav-hamburger span:nth-child(3) { top: 24px; }
.nav-hamburger.open span:nth-child(1) { top: 19px; transform: rotate(45deg); }
.nav-hamburger.open span:nth-child(2) { opacity: 0; }
.nav-hamburger.open span:nth-child(3) { top: 19px; transform: rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 49;
  background: rgb(var(--black-ch) / 0.88);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  opacity: 0;
  visibility: hidden;
  transition: opacity .35s ease, visibility 0s linear .35s;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 40px;
  padding: 0 32px;
}
.mobile-menu.open {
  opacity: 1;
  visibility: visible;
  transition: opacity .35s ease;
}
.mobile-menu::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--accent-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--accent-grid) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%);
  pointer-events: none;
}
.mobile-menu a {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--neutral-100);
  position: relative;
  padding: 6px 0;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity .5s ease, transform .5s cubic-bezier(.2,.8,.2,1), color .25s ease;
}
.mobile-menu.open a {
  opacity: 1;
  transform: translateY(0);
}
.mobile-menu.open a:nth-child(1) { transition-delay: .15s; }
.mobile-menu.open a:nth-child(2) { transition-delay: .22s; }
.mobile-menu.open a:nth-child(3) { transition-delay: .29s; }
.mobile-menu.open .mobile-menu-cta { transition-delay: .36s; }
.mobile-menu a:hover { color: var(--accent); }
.mobile-menu a::after {
  content: "";
  position: absolute;
  left: 50%; bottom: 0;
  width: 0; height: 1px;
  background: var(--accent);
  transform: translateX(-50%);
  transition: width .25s ease;
}
.mobile-menu a:hover::after { width: 60%; }
.mobile-menu-cta {
  padding: 14px 32px !important;
  border-radius: 999px;
  border: 1px solid var(--accent);
  background: linear-gradient(135deg, rgb(var(--accent-ch) / 0.15), rgb(var(--accent-ch) / .05));
  font-size: 16px !important;
  color: var(--neutral-100) !important;
  margin-top: 8px;
}
.mobile-menu-foot {
  position: absolute;
  bottom: 32px;
  left: 0; right: 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.32em;
  color: var(--text-faint);
  text-transform: uppercase;
}

@media (max-width: 860px) {
  .nav-hamburger { display: flex; }
}
/* Prevent body scroll when menu is open */
body.menu-open { overflow: hidden; }

/* ---------- HERO ---------- */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
  z-index: 1;
}
.hero-canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.hero-canvas canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
}
.hero-vignette {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(ellipse at 30% 50%, transparent 0%, rgb(var(--black-ch) / 0.6) 70%),
    linear-gradient(180deg, transparent 60%, var(--bg) 100%);
}
.hero-inner {
  position: relative;
  z-index: 2;
  padding: 140px 0 100px;
  width: 100%;
  pointer-events: none;
}
.hero-inner > * { pointer-events: auto; }
.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: rgb(var(--white-ch) / 0.02);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.24em;
  color: var(--neutral-300);
  text-transform: uppercase;
  margin-bottom: 28px;
}
.hero-eyebrow .dot {
  width: 6px; height: 6px;
  background: var(--accent);
  border-radius: 999px;
  box-shadow: 0 0 12px var(--accent);
  animation: pulse 2s infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
.hero-headline {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' var(--display-width);
  font-size: var(--hero-size);
  line-height: var(--hero-leading);
  font-weight: 650;
  letter-spacing: var(--hero-tracking);
  max-width: 14ch;
  margin-bottom: 28px;
  color: var(--neutral-000);
}
/* Flat gold, no faux italic. The oblique was synthesised, and had to be disabled
   for Chinese anyway — which left EN and zh speaking with two different voices. */
.hero-headline .engineered {
  font-weight: 650;
  color: var(--accent);
}
.hero-sub {
  max-width: 56ch;
  color: var(--text-dim);
  font-size: clamp(15px, 1.4vw, 18px);
  margin-bottom: 40px;
}
.hero-ctas {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 64px;
}
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 16px 26px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.01em;
  transition: all .3s ease;
  position: relative;
  overflow: hidden;
}
.btn-primary {
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
  color: var(--on-accent);
  font-weight: 600;
  box-shadow: 0 0 0 1px rgb(var(--accent-ch) / 0.35), 0 10px 28px -12px rgb(var(--accent-ch) / 0.35);
}
.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 0 0 1px rgb(var(--accent-ch) / 0.55), 0 14px 36px -12px rgb(var(--accent-ch) / 0.45);
}
.btn-primary .arr { transition: transform .25s ease; }
.btn-primary:hover .arr { transform: translateX(4px); }
.btn-secondary {
  background: rgb(var(--white-ch) / 0.02);
  border: 1px solid var(--line-strong);
  color: var(--neutral-100);
}
.btn-secondary:hover {
  border-color: var(--neutral-300);
  background: rgb(var(--white-ch) / 0.05);
}
.hero-meta {
  display: flex;
  gap: 48px;
  flex-wrap: wrap;
  padding-top: 32px;
  border-top: 1px solid var(--line);
  max-width: 720px;
}
.hero-meta-item .k {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.32em;
  color: var(--text-faint);
  text-transform: uppercase;
  margin-bottom: 6px;
}
.hero-meta-item .v {
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--neutral-100);
  font-weight: 500;
}

/* scroll cue */
.scroll-cue {
  position: absolute;
  bottom: 30px; left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.32em;
  color: var(--text-faint);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.scroll-cue .line {
  width: 1px; height: 40px;
  background: linear-gradient(180deg, var(--accent) 0%, transparent 100%);
  animation: drop 2.4s ease-in-out infinite;
}
@keyframes drop {
  0% { transform: scaleY(0); transform-origin: top; }
  50% { transform: scaleY(1); transform-origin: top; }
  51% { transform-origin: bottom; }
  100% { transform: scaleY(0); transform-origin: bottom; }
}

/* On narrow viewports the bull centers behind the text, but stays visible. */
@media (max-width: 980px) {
  .hero-canvas { opacity: 0.4; }
  .hero-inner { text-align: center; }
  .hero-inner .hero-eyebrow,
  .hero-inner .hero-headline,
  .hero-inner .hero-sub,
  .hero-inner .hero-ctas,
  .hero-inner .hero-meta {
    margin-left: auto;
    margin-right: auto;
  }
  .hero-inner .hero-headline { max-width: 100%; }
  .hero-inner .hero-sub { max-width: 56ch; }
  .hero-inner .hero-ctas { justify-content: center; }
  .hero-inner .hero-meta { justify-content: center; }
  .hero-vignette {
    background:
      radial-gradient(ellipse at 50% 50%, transparent 0%, rgb(var(--black-ch) / 0.78) 70%),
      linear-gradient(180deg, transparent 60%, var(--bg) 100%) !important;
  }
}

/* ---------- SECTION HEADERS ---------- */
.section { padding: 140px 0; position: relative; z-index: 1; }
.section-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--eyebrow-tracking);
  color: var(--accent);
  text-transform: uppercase;
  margin-bottom: 20px;
}
.section-eyebrow::before {
  content: "";
  width: 28px; height: 1px;
  background: currentColor;
}
.section-title {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' var(--display-width);
  font-size: clamp(32px, 4.4vw, 56px);
  line-height: 1.06;
  font-weight: 650;
  letter-spacing: var(--title-tracking);
  margin-bottom: 18px;
  color: var(--text);
  max-width: 22ch;
}
.section-lead {
  color: var(--text-dim);
  max-width: 60ch;
  font-size: 16px;
}

/* ---------- SERVICES — the ledger ---------- */
/* Four rows on a rail, not four cards. Each row is a node on the system trace,
   which is what finally makes the 01-04 numbering encode something. A ledger
   also lets Bahasa Melayu's long service names take a full row instead of
   being squeezed into a fixed-width card. */
.services-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 48px;
  margin-bottom: 64px;
  flex-wrap: wrap;
}

/* Trace lead-in: the line arrives from the hero before the first node. */
.trace-lead {
  width: 2px;
  height: 56px;
  margin-left: 21px;
  background: linear-gradient(180deg, transparent, var(--accent));
  opacity: .55;
}

.services-ledger {
  position: relative;
  border-top: 1px solid var(--line);
}
/* The trace rail: one continuous gold line down the left of the ledger. */
.services-ledger::before {
  content: "";
  position: absolute;
  left: 21px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent);
  opacity: .55;
}

.service-row {
  position: relative;
  display: grid;
  grid-template-columns: 44px 52px minmax(180px, 1.1fr) minmax(0, 2fr) auto;
  align-items: center;
  gap: 28px;
  padding: 26px 0;
  border-bottom: 1px solid var(--line);
  transition: background .3s ease;
}
.service-row:hover { background: rgb(var(--surface-3-ch) / .55); }

/* The node where the trace forks into this row. */
.service-num {
  position: relative;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--eyebrow-tracking);
  color: var(--text-faint);
  padding-left: 30px;
}
.service-num::before {
  content: "";
  position: absolute;
  left: -2px; top: 50%;
  transform: translateY(-50%);
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
}
.service-num::after {
  content: "";
  position: absolute;
  left: 8px; top: 50%;
  width: 16px; height: 2px;
  background: var(--accent);
  opacity: .55;
}

/* Stamped chip — an icon you can actually read. */
.service-chip {
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  background: var(--plane);
  border: 1px solid var(--line);
  border-radius: 4px;
}
.service-icon {
  width: 32px; height: 32px;
  color: var(--neutral-300);
}
/* Silver strokes are the chassis; the single gold stroke is the signal path
   through it — the same semantics as the page trace. */
.service-icon .signal { stroke: var(--accent); }

.service-name {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' var(--display-width);
  font-size: 19px;
  font-weight: 600;
  color: var(--neutral-100);
  letter-spacing: -0.01em;
}
.service-desc {
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.65;
}
.service-link {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--eyebrow-tracking);
  color: var(--text-faint);
  text-transform: uppercase;
  white-space: nowrap;
  transition: color .2s ease;
}
.service-row:hover .service-link { color: var(--accent); }

@media (max-width: 900px) {
  .service-row {
    grid-template-columns: 44px 52px 1fr;
    row-gap: 12px;
    align-items: start;
  }
  .service-name { align-self: center; }
  .service-desc { grid-column: 3 / -1; }
  .service-link { grid-column: 3 / -1; }
}
@media (max-width: 560px) {
  /* The rail has to keep meeting the node stubs. Hiding the stub here left the
     nodes floating free of the line. */
  .services-ledger::before { left: 14px; }
  .trace-lead { margin-left: 14px; }
  .service-row { grid-template-columns: 34px 44px 1fr; gap: 14px; }
  .service-num { padding-left: 24px; font-size: 10px; }
  .service-num::after { width: 10px; }
  .service-chip { width: 40px; height: 40px; }
  .service-icon { width: 28px; height: 28px; }
}
/* ---------- CONTACT ---------- */
.contact-section {
  padding: 140px 0;
  position: relative;
  overflow: hidden;
}
.contact-section::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at 80% 20%, rgb(var(--accent-ch) / 0.08) 0%, transparent 50%),
    radial-gradient(ellipse at 20% 80%, rgb(var(--neutral-ch) / 0.04) 0%, transparent 50%);
  pointer-events: none;
}
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 80px;
  position: relative;
  /* Without this the form stretches to the height of the taller info column,
     leaving a large empty panel below the submit button. */
  align-items: start;
}
@media (max-width: 980px) {
  .contact-grid { grid-template-columns: 1fr; gap: 56px; }
}
.contact-info p {
  color: var(--text-dim);
  margin-bottom: 36px;
  font-size: 17px;
  max-width: 40ch;
}
/* Where the trace terminates. The rail returns alongside the contact rows and
   ends in a filled port — the same grammar as the service ledger nodes, closing
   the line that started under the bull. */
.contact-list {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding-left: 28px;
}
.contact-list::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 0;
  height: calc(100% - 26px);
  width: 2px;
  background: var(--accent);
  opacity: .55;
}
.contact-list::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 18px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
}
@media (max-width: 560px) {
  .contact-list { padding-left: 22px; }
}
.contact-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 0;
  border-top: 1px solid var(--line);
}
.contact-row .lbl {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--text-faint);
  width: 80px;
  flex-shrink: 0;
}
.contact-row .val {
  font-family: var(--font-display);
  font-size: 16px;
  color: var(--neutral-100);
  font-weight: 500;
}

/* An opaque plane, not glass. The form used to be a 45%-alpha wash sitting over
   a WebGL torus, which cost it both legibility and any sense of being a surface. */
.form {
  padding: 40px;
  background: var(--plane);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-bottom: 18px;
}
@media (max-width: 600px) {
  .form-row { grid-template-columns: 1fr; }
  .form { padding: 28px; }
}
.field { display: flex; flex-direction: column; }
.field label {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-faint);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  margin-bottom: 10px;
}
.field input,
.field select,
.field textarea {
  background: rgb(var(--black-ch) / 0.5);
  border: 1px solid var(--line);
  color: var(--neutral-100);
  padding: 14px 16px;
  border-radius: 10px;
  font-family: var(--font-body);
  font-size: 14px;
  transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
  outline: none;
}
.field textarea { resize: vertical; min-height: 120px; }
.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-veil);
  background: rgb(var(--black-ch) / 0.7);
}
.field select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--neutral-500) 50%), linear-gradient(135deg, var(--neutral-500) 50%, transparent 50%); background-position: calc(100% - 18px) center, calc(100% - 12px) center; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; padding-right: 36px; }
.form .btn-primary { width: 100%; justify-content: center; margin-top: 8px; }

/* ---------- FOOTER ---------- */
footer {
  border-top: 1px solid var(--line);
  padding: 60px 0 40px;
  position: relative;
  z-index: 1;
}
.footer-inner {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 32px;
  flex-wrap: wrap;
}
.foot-brand { max-width: 380px; }
.foot-brand p {
  color: var(--text-faint);
  font-size: 13px;
  margin-top: 18px;
}
.foot-links {
  display: flex;
  gap: 28px;
}
.foot-links a {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  transition: color .2s ease;
}
.foot-links a:hover { color: var(--accent); }
.copyright {
  margin-top: 56px;
  padding-top: 24px;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  color: var(--text-faint);
  text-transform: uppercase;
}

/* ---------- TOAST ---------- */
.toast {
  position: fixed;
  bottom: 32px;
  right: 32px;
  z-index: 100;
  padding: 16px 22px;
  background: var(--plane);
  border: 1px solid var(--line);
  border-radius: 12px;
  color: var(--neutral-100);
  font-size: 13px;
  box-shadow: 0 20px 60px -10px rgb(var(--black-ch) / .7);
  transform: translateY(20px);
  opacity: 0;
  pointer-events: none;
  transition: all .35s cubic-bezier(.2,1,.3,1);
  display: flex;
  gap: 12px;
  align-items: center;
  max-width: 360px;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast .check {
  width: 22px; height: 22px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.toast .check svg { width: 12px; height: 12px; }
.toast strong { display: block; font-weight: 600; margin-bottom: 2px; }
.toast small { color: var(--text-dim); font-size: 11px; }

/* ---------- REVEAL ---------- */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .8s ease, transform .8s cubic-bezier(.2,.7,.2,1);
}
.reveal.in { opacity: 1; transform: translateY(0); }

/* ---------- DIVIDER ---------- */
.div-line {
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-strong) 30%, var(--line-strong) 70%, transparent);
  margin: 0 auto;
  max-width: 1280px;
}
/* (the floating 3D icon rig is gone — see .service-chip above) */
/* ---------- PORTFOLIO — the vellum act ---------- */
/* The one light section on the page. Drafting paper carrying ink schematics:
   it breaks the dark tunnel, gives the page a second act, and is the only
   ground on which the architecture drawings are legible. */
.portfolio-section {
  background: var(--paper);
  color: var(--ink);
  padding: 140px 0;
  position: relative;
  z-index: 1;
}
/* Faint drafting grid on the paper itself. */
.portfolio-section::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--paper-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--paper-line) 1px, transparent 1px);
  background-size: 32px 32px;
  opacity: .45;
  pointer-events: none;
}
.portfolio-inner { position: relative; z-index: 2; }

.portfolio-section .section-title { color: var(--ink); }
.portfolio-section .section-eyebrow { color: var(--ink-dim); }
.portfolio-section .section-lead { color: var(--ink-dim); }

/* ---------- PORTFOLIO SLIDER ---------- */
/* A scroll-snap track: native swipe and momentum on touch, arrows and keyboard
   on desktop, and it wraps around at both ends. Two cards visible on desktop so
   the screenshots stay legible; one on a phone. */
.port-slider { position: relative; }

.portfolio-grid {
  display: flex;
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* The first and last card must be able to reach dead centre, or the browser
     snaps past them on load and the deck opens on card two. That needs side
     padding of exactly half the leftover space.
     Card width is set in vw/px, not %, so it does not depend on this padding —
     a percentage basis would resolve against the content box and make the two
     values circular. */
  --card-w: clamp(280px, 74vw, 560px);
  padding-block: 36px 40px;
  padding-inline: max(0px, calc((100% - var(--card-w)) / 2));
}
.portfolio-grid::-webkit-scrollbar { display: none; }
.portfolio-grid:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}
.portfolio-grid > .port-card {
  flex: 0 0 var(--card-w);
  scroll-snap-align: center;
}
@media (max-width: 1000px) { .portfolio-grid { --card-w: clamp(260px, 78vw, 600px); } }
/* On a phone only one card fits, so narrow it enough that the next one peeks —
   otherwise nothing signals that the deck scrolls. Needs the gap tightened too,
   or the neighbour lands just past the edge. */
@media (max-width: 640px) {
  .portfolio-grid { --card-w: clamp(240px, 71vw, 620px); gap: 12px; }
}

.port-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  margin-top: 28px;
}
.port-arrow {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border: 1px solid var(--ink-dim);
  border-radius: 50%;
  color: var(--ink);
  background: rgb(var(--vellum-ch) / .7);
  transition: background .2s ease, border-color .2s ease;
}
.port-arrow svg { width: 20px; height: 20px; }
.port-arrow:hover { background: var(--paper-line); border-color: var(--ink); }
.port-arrow:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.port-dots { display: flex; gap: 8px; }
.port-dots i {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--paper-line);
  border: 1px solid var(--ink-dim);
  transition: background .2s ease;
}
.port-dots i.on { background: var(--ink); }

/* The card is the snap target and must never be transformed: scroll-snap areas
   are measured from the *transformed* border box, so animating the card itself
   moves its own snap position while scrolling — a feedback loop that made the
   track overshoot to the end. All motion lives on .port-inner instead. */
.port-card { background: none; border: 0; }

.port-inner {
  height: 100%;
  background: rgb(var(--vellum-ch) / .75);
  border: 1px solid var(--paper-line);
  border-radius: 2px;
  overflow: hidden;
  transition: border-color .3s ease;
}
.port-card:hover .port-inner { border-color: var(--ink-dim); }

/* The drawing sheet. */
.port-sheet {
  position: relative;
  aspect-ratio: 16 / 10;
  border-bottom: 1px solid var(--paper-line);
}

/* Engineering title block — absorbs the old redundant pill label. */
.port-titleblock {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--paper-line);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: var(--eyebrow-tracking);
  color: var(--ink-dim);
  text-transform: uppercase;
}

.port-body { padding: 20px 22px 24px; }
.port-title {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' var(--display-width);
  font-size: 19px;
  font-weight: 650;
  color: var(--ink);
  letter-spacing: -0.01em;
  margin-bottom: 10px;
}
.port-desc {
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-dim);
  margin-bottom: 16px;
}
/* ---------- LANGUAGE SWITCHER ---------- */
/* Markup is generated by build.mjs (computed().langSwitcher) — see the
   {{langSwitcher}} token in src/partials/nav.html. */
.lang-dd { position: relative; }

.lang-dd-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--text-dim);
  transition: color 0.25s, border-color 0.25s, background 0.25s;
}
.lang-dd-btn:hover {
  color: var(--neutral-100);
  border-color: var(--accent-border);
  background: var(--accent-wash);
}
.lang-dd-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-veil);
}
.lang-globe { width: 15px; height: 15px; flex: none; }
.lang-caret {
  width: 10px; height: 10px; flex: none;
  transition: transform 0.25s;
}
.lang-dd.open .lang-caret { transform: rotate(180deg); }

.lang-dd-menu {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  min-width: 168px;
  padding: 6px;
  margin: 0;
  list-style: none;
  border-radius: 12px;
  border: 1px solid var(--line-strong);
  background: linear-gradient(180deg, rgb(var(--surface-2-ch) / .98), rgb(var(--surface-ch) / .98));
  backdrop-filter: blur(18px);
  box-shadow: 0 24px 60px -18px rgb(var(--black-ch) / .8);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity 0.22s, transform 0.22s, visibility 0.22s;
  z-index: 60;
}
.lang-dd.open .lang-dd-menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.lang-dd-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 13px;
  color: var(--text-dim);
  transition: color 0.2s, background 0.2s;
}
.lang-dd-item:hover { color: var(--neutral-100); background: var(--accent-wash); }
.lang-dd-item.is-current { color: var(--accent); }
.lang-dd-item svg { width: 14px; height: 14px; flex: none; }

/* In the mobile drawer the switcher sits inline, opening downward in flow. */
.mobile-menu .lang-dd { margin-top: 18px; }
.mobile-menu .lang-dd-menu { right: auto; left: 0; }

/* ==========================================================================
   PER-LOCALE TYPOGRAPHY
   Driven by <html lang> — one stylesheet serves all three locales, no JS.
   Values override the knobs declared in tokens.css.
   ========================================================================== */

/* ---------- Bahasa Melayu ---------- */
/* BM runs long ("Penyelesaian IT Sehenti", "Pembangunan Laman Web"). Archivo's
   width axis absorbs that directly: narrowing the display face buys horizontal
   room without the letter-spacing hacks the old face needed. */
:root:lang(ms) {
  --display-width: 104;
  --eyebrow-tracking: 0.1em;
  --nav-gap: 24px;
}
:lang(ms) .hero-headline { max-width: 18ch; }
:lang(ms) .section-title { max-width: 26ch; }
/* The hamburger has to appear earlier than in English or the nav crowds. */
@media (max-width: 1000px) {
  :lang(ms) .nav-links { display: none; }
  :lang(ms) .nav-cta { display: none; }
  :lang(ms) .nav-hamburger { display: flex; }
}

/* ---------- Simplified Chinese ---------- */
/* Hanzi are dense squares: negative tracking collides them, 0.98 leading clips
   them, and an equal point size reads visually larger than Latin. */
:root:lang(zh) {
  --font-display: var(--font-cjk);
  --font-body: var(--font-cjk);
  --hero-size: clamp(34px, 5.6vw, 88px);
  --hero-leading: 1.15;
  --hero-tracking: 0;
  --title-tracking: 0;
  --body-leading: 1.75;
  --eyebrow-tracking: 0.2em;
}
/* Noto Sans SC has no variable width axis; neutralise the knob so the Latin
   default cannot leak in as a synthesised stretch. */
:lang(zh) .hero-headline,
:lang(zh) .section-title,
:lang(zh) .service-name,
:lang(zh) .port-title,
:lang(zh) .brand-name { font-variation-settings: normal; }
:lang(zh) .hero-headline { max-width: 16ch; }
:lang(zh) .section-title { max-width: 20ch; line-height: 1.25; }
/* JetBrains Mono has no hanzi. Translated labels fall back to the CJK face, so
   give them the wide-tracked titling treatment rather than a broken mono look. */
:lang(zh) .section-eyebrow,
:lang(zh) .hero-eyebrow,
:lang(zh) .field label {
  font-family: var(--font-cjk);
  font-weight: 500;
}
/* Structural chrome (/ 01, SCROLL, port-meta years) stays Latin in every
   locale, so it keeps the mono face. */
:lang(zh) .service-num,
:lang(zh) .port-meta,
:lang(zh) .scroll-cue,
:lang(zh) .brand-sub {
  font-family: var(--font-mono);
}

/* ==========================================================================
   REDUCED MOTION
   The hero's bloomed bull and the reveal animations run by default.
   Honour the OS preference.
   The JS side (hero-bull.js) renders a single frame
   and stops its RAF loops when this same query matches.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  /* .reveal starts at opacity 0 and is animated in — without the animation it
     must be visible immediately, or the page reads as blank. */
  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  .port-card { transform: none !important; }

  /* The trace is drawn with stroke-dashoffset. Under reduced motion it must
     present already complete rather than frozen part-way. */
  .trace-draw {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }
}

/* Same guarantee for anyone reaching the page with JS disabled or failed:
   the reveal animation must never be what keeps content off the screen. */
.no-js .reveal {
  opacity: 1;
  transform: none;
}

/* WebGL failed to initialise (or was skipped for reduced motion) — the hero
   must not be left as an empty black box. */
.hero.webgl-failed .hero-canvas {
  background:
    radial-gradient(ellipse at 70% 40%, var(--accent-wash) 0%, transparent 60%),
    radial-gradient(ellipse at 30% 70%, rgb(var(--accent-ch) / .03) 0%, transparent 60%);
}

/* A screenshot mounted where a drawing would otherwise sit. Same frame, same
   title block — a technical dossier carries both drawings and photo plates. */
/* The ratio belongs to the image, not to the padded sheet. Putting it on the
   sheet made the inner box 1.674 and cover clipped 10px off every screenshot. */
.port-sheet--shot {
  padding: 12px;
  background: rgb(var(--vellum-ch) / .6);
}
.port-sheet--shot img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  display: block;
  object-fit: cover;
  border: 1px solid var(--ink-dim);
}

/* ---------- SLIDER DEPTH ---------- */
/* Cards sit back and dim as they enter or leave the track, and come forward
   when they are comfortably in view. Driven by the scroll position itself
   (animation-timeline: view), so it runs on the compositor and schedules no
   animation frames — the reduced-motion guarantee is unaffected.

   Animates the individual `scale` and `translate` properties rather than
   `transform`, which leaves `transform` free for the pointer tilt below. The
   two compose instead of fighting.

   @supports keeps this to browsers that have scroll-driven animations
   (~85% today); everywhere else the slider stays flat and works exactly as now. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .portfolio-grid { perspective: 1400px; }

    .portfolio-grid > .port-card > .port-inner {
      animation: card-depth linear both;
      animation-timeline: view(inline);
      view-timeline-inset: 0;
      transform-style: preserve-3d;
      will-change: scale, translate, opacity;
    }

    /* view(inline) puts the element dead centre of the scrollport at 50%, so
       that is where the card is full size and fully opaque. Either side of it
       the siblings recede and dim, which is what reads as depth. */
    @keyframes card-depth {
      0%   { scale: .80; translate: 0 0 -180px; rotate: y  16deg; opacity: .35; }
      50%  { scale: 1;   translate: 0 0 0;      rotate: y   0deg; opacity: 1; }
      100% { scale: .80; translate: 0 0 -180px; rotate: y -16deg; opacity: .35; }
    }
  }
}

/* Pointer tilt — desktop only, and only for visitors who accept motion.
   JS writes the transform; see app.js. */
.port-inner {
  transition: transform .35s cubic-bezier(.2, .8, .2, 1), border-color .3s ease;
}
.port-card[data-tilting] .port-inner { transition: border-color .3s ease; }

/* ---------- THE TRACE DRAWS ITSELF ---------- */
/* The gold line is the page's signature: it leaves the hero, forks a node into
   each service row, and terminates beside the contact details. Static, it is
   just a rule. Drawn by the scroll position, it reads as a circuit being
   traced — which is the whole idea.

   Scroll-driven, so it runs on the compositor and schedules no animation
   frames. Animates `scale` (the individual property) rather than `transform`,
   leaving transform free and avoiding any conflict.

   Without scroll-driven animation support, or with reduced motion on, the line
   is simply drawn in full from the start — the design still reads. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .trace-lead,
    .services-ledger::before,
    .contact-list::before {
      transform-origin: top center;
      animation: trace-draw linear both;
    }
    .trace-lead { animation-timeline: view(); animation-range: entry 0% cover 30%; }
    .contact-list::before { animation-timeline: view(); animation-range: entry 10% cover 60%; }

    /* The ledger publishes one timeline that both the rail and its nodes read,
       so each node lights up exactly as the line sweeps past it. On separate
       view() timelines they fired on their own entry instead, and the last node
       lit before the line had reached it. */
    .services-ledger { view-timeline-name: --ledger; }
    .services-ledger::before {
      animation-timeline: --ledger;
      animation-range: cover 10% cover 75%;
    }

    .service-num::before {
      animation: node-arrive linear both;
      animation-timeline: --ledger;
    }
    .service-row:nth-child(1) .service-num::before { animation-range: cover 18% cover 26%; }
    .service-row:nth-child(2) .service-num::before { animation-range: cover 33% cover 41%; }
    .service-row:nth-child(3) .service-num::before { animation-range: cover 48% cover 56%; }
    .service-row:nth-child(4) .service-num::before { animation-range: cover 63% cover 71%; }

    .contact-list::after {
      animation: node-arrive linear both;
      animation-timeline: view();
      animation-range: entry 40% cover 55%;
    }

    @keyframes trace-draw {
      from { scale: 1 0; }
      to   { scale: 1 1; }
    }
    @keyframes node-arrive {
      from { scale: 0; opacity: 0; }
      to   { scale: 1; opacity: 1; }
    }
  }
}
