/* ==================================================================
   AITEN Embodied Robot — Cinematic Launch Page Styles
   SpaceX / Apple-style product narrative: fullscreen hero, scroll-driven
   reveals, full-bleed imagery, and a single disciplined light signature.
   ================================================================== */

/* ------------------------------- reset ------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--c-void);
  color: var(--c-ink);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: var(--fw-regular);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; cursor: pointer; }
a { color: inherit; text-decoration: none; }
dl, dd, dt { margin: 0; }
h1, h2, h3, p { margin: 0; }
[hidden] { display: none; }

::selection { background: var(--c-accent); color: var(--c-void); }

/* --------------------------- no-script fallback --------------------------- */
.no-script {
  max-width: 44ch;
  margin: 18vh auto 0;
  padding: 0 var(--s-5);
  text-align: center;
  color: var(--c-ink-2);
}

.no-script p { font-size: var(--fs-lead); line-height: 1.55; }
.no-script p + p { margin-top: var(--s-3); color: var(--c-ink-3); }

/* ------------------------------ typography ------------------------------ */
.eyebrow {
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
  font-weight: var(--fw-medium);
}

/* -------------------------------- header -------------------------------- */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--header-h);
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: 0 var(--gutter);
  background: linear-gradient(180deg, rgba(4, 7, 11, 0.85) 0%, rgba(4, 7, 11, 0) 100%);
  backdrop-filter: blur(8px);
  transition: background var(--dur-base) var(--ease-out);
}

.brand {
  display: inline-flex;
  align-items: center;
  color: var(--c-ink);
  text-decoration: none;
}

.brand__logo {
  display: block;
  height: 24px;
  width: auto;
}

.proposal-switch,
.lang-switch {
  display: flex;
  align-items: center;
  gap: var(--s-1);
}

.proposal-switch {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  padding: 3px;
  border: var(--hairline);
  border-radius: var(--r-pill);
  background: rgba(16, 24, 34, 0.5);
}

.proposal-switch button {
  border: 0;
  background: transparent;
  color: var(--c-ink-3);
  padding: 7px 18px;
  border-radius: var(--r-pill);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-nav);
  text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}

.proposal-switch button:hover { color: var(--c-ink-2); }

.proposal-switch button.is-active {
  background: var(--c-surface-2);
  color: var(--c-ink);
  box-shadow: inset 0 0 0 1px var(--c-line-strong);
}

/* Language: a compact globe trigger that unfurls a small menu, so the
   header stays calm instead of showing three parallel text buttons. */
.lang-switch {
  position: relative;
}

.lang-switch__trigger {
  display: flex;
  align-items: center;
  gap: 7px;
  border: var(--hairline);
  background: rgba(16, 24, 34, 0.5);
  color: var(--c-ink-2);
  padding: 6px 12px;
  border-radius: var(--r-pill);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-nav);
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}

.lang-switch__trigger:hover {
  color: var(--c-ink);
  border-color: var(--c-line-strong);
}

.lang-switch__globe {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.8;
}

.lang-switch__current {
  min-width: 2.2em;
  text-align: left;
}

.lang-switch__caret {
  width: 12px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.7;
  transition: transform var(--dur-fast) var(--ease-out);
}

.lang-switch.is-open .lang-switch__caret { transform: rotate(180deg); }

.lang-switch__menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 100%;
  padding: 5px;
  border: var(--hairline);
  border-radius: var(--r-3);
  background: rgba(12, 18, 26, 0.94);
  backdrop-filter: blur(14px);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.7);
  display: flex;
  flex-direction: column;
  gap: 2px;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              visibility var(--dur-fast);
}

.lang-switch.is-open .lang-switch__menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.lang-switch__menu button {
  display: block;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--c-ink-3);
  padding: 8px 10px;
  border-radius: var(--r-2);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-nav);
  text-align: left;
  transition: color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}

.lang-switch__menu button:hover {
  color: var(--c-ink);
  background: var(--c-surface-2);
}

.lang-switch__menu button.is-active { color: var(--c-accent); }

/* ------------------------------ side nav ------------------------------ */
/* A vertical rail pinned to the right edge of the viewport. It stays
   COLLAPSED (just a thin handle) while the 3D hero is on screen, and only
   unfurls its section list once the page has scrolled into the detail
   sections — so the hero's right edge stays clear for the model. */
.side-nav {
  position: fixed;
  right: 0;
  top: 50%;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* Parked off the right edge until the reader leaves the 3D hero. */
  opacity: 0;
  pointer-events: none;
  transform: translateY(-50%) translateX(100%);
  transition: opacity var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out);
}
.side-nav.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(-50%) translateX(0);
}

.side-nav__toggle {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 62px;
  padding: 0;
  border: 0;
  background: none;
}
/* Collapsed, the handle is a short, fat glowing capsule floating against the
   right edge. It has no dots and no long axis — the section list is the only
   thing that ever carries the waypoints. Opening the list makes the capsule
   itself disappear so the labels stand on their own. */
.side-nav__capsule {
  position: absolute;
  width: 10px;
  height: 34px;
  border-radius: 999px;
  background: linear-gradient(180deg, #d9f6ff 0%, #3ad1f6 55%, rgba(58, 209, 246, 0.35) 100%);
  box-shadow: 0 0 0 1px rgba(143, 233, 255, 0.25),
              0 0 10px 2px rgba(53, 211, 255, 0.4),
              0 0 26px 6px rgba(53, 211, 255, 0.16);
  animation: nav-float 2s var(--ease-inout) infinite;
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.side-nav__toggle:hover .side-nav__capsule {
  box-shadow: 0 0 0 1px rgba(143, 233, 255, 0.4),
              0 0 14px 3px rgba(53, 211, 255, 0.55),
              0 0 32px 9px rgba(53, 211, 255, 0.22);
}
/* The capsule stays put once the list is open and keeps bobbing; it IS the
   toggle, so it doubles as the affordance for tucking the list back in. Its
   glow just lifts a touch to read as "active". */
.side-nav.is-open .side-nav__capsule {
  box-shadow: 0 0 0 1px rgba(143, 233, 255, 0.35),
              0 0 12px 3px rgba(53, 211, 255, 0.5),
              0 0 28px 8px rgba(53, 211, 255, 0.2);
}

/* The panel unfurls out to the LEFT of the handle, sharing the same right
   edge, so the rail never leaves the viewport's right side. It carries no
   surface of its own — just the labels, right-aligned — so they read as part
   of the page rather than as a floating card. */
.side-nav__panel {
  position: absolute;
  top: 50%;
  right: 100%;
  transform: translateY(-50%) translateX(8px);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s-4);
  text-align: right;
  padding: var(--s-2) var(--s-4) var(--s-2) 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.side-nav.is-open .side-nav__panel {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
  pointer-events: auto;
}
.side-nav.is-open .side-nav__toggle {
  color: var(--c-accent);
  border-color: var(--c-accent);
}

/* The word "Sections" only earns its place as an aria-label on the nav; as a
   visible caption above four numbered links it is pure noise. */
.side-nav__label { display: none; }

.side-nav__links {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.side-nav__links a {
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: var(--s-3);
  padding: 7px 0;
  font-size: var(--fs-small);
  letter-spacing: 0.02em;
  color: var(--c-ink-3);
  /* CJK labels have no spaces to break on, so without nowrap the panel (which
     sizes to content) collapses and the label stacks one glyph per line. */
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out);
}

.side-nav__text {
  white-space: nowrap;
}

.side-nav__no {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  color: inherit;
  transition: color var(--dur-fast) var(--ease-out);
}

/* A short cyan tick slides in from the right on hover / active, so the
   current section is marked without a boxed highlight. */
.side-nav__links a::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 3px;
  left: 0;
  height: 1px;
  background: var(--c-accent);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-fast) var(--ease-out);
}

.side-nav__links a:hover { color: var(--c-ink); }

.side-nav__links a.is-active { color: var(--c-accent); }
.side-nav__links a.is-active::after { transform: scaleX(1); }

/* --------------------------- mobile jump bar --------------------------- */
.mobile-jump { display: none; }

/* -------------------------------- main -------------------------------- */
main { display: block; }

/* ------------------------------ overview ------------------------------ */
.overview {
  position: relative;
  min-height: var(--section-min-h);
  min-height: var(--section-min-h-svh);
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  align-items: center;
  padding: calc(var(--header-h) + var(--s-6)) var(--gutter) var(--s-8);
  overflow: hidden;
}

.overview__bg {
  position: absolute;
  inset: 0;
  z-index: var(--z-bg);
  pointer-events: none;
}

.overview__glow {
  position: absolute;
  top: -22%;
  left: 57%;
  transform: translateX(-50%);
  width: 130vw;
  height: 120vh;
  background:
    radial-gradient(
      ellipse 31% 39% at 62% 48%,
      rgba(27, 153, 255, 0.32) 0%,
      rgba(20, 121, 220, 0.15) 38%,
      rgba(8, 48, 100, 0) 70%
    ),
    radial-gradient(
      ellipse 52% 12% at 62% 79%,
      rgba(41, 188, 255, 0.30) 0%,
      rgba(16, 93, 180, 0.13) 43%,
      rgba(0, 38, 90, 0) 76%
    );
  opacity: 0.94;
  pointer-events: none;
}

.overview__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--c-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--c-line) 1px, transparent 1px);
  background-size: 120px 120px;
  mask-image: radial-gradient(ellipse at 50% 55%, #000 0%, transparent 65%);
  -webkit-mask-image: radial-gradient(ellipse at 50% 55%, #000 0%, transparent 65%);
  opacity: 0.35;
}

.overview__vignette {
  position: absolute;
  inset: 0;
  background: var(--vignette);
  pointer-events: none;
}

.overview__scanlines {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(53, 211, 255, 0.015) 2px,
    rgba(53, 211, 255, 0.015) 4px
  );
  opacity: 0.6;
  animation: scan 18s linear infinite;
  pointer-events: none;
}

@keyframes scan {
  0% { transform: translateY(0); }
  100% { transform: translateY(8px); }
}

/* ---------------------------- model stage ---------------------------- */
.overview__stage {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  display: grid;
  place-items: center;
  padding: calc(var(--header-h) + var(--s-4)) 0 var(--s-10);
}

.model-stage {
  position: relative;
  width: 100%;
  /* Fill the whole hero stage (which is inset:0 on #overview) so the model can
     be dragged anywhere across the hero, not just the central band. The stage
     is transparent and the copy column sits on top with pointer-events:none,
     so the larger hit area does not swallow hero copy interactions. */
  height: 100%;
  border-radius: 0;
  background: transparent;
}

.model-stage canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* On mobile the control rail shares the stage as a flex item, so the
     flex-grow canvas can be squeezed to nothing and take the model with it.
     An absolute canvas takes the stage box instead and leaves the rail its
     own row. */
  position: absolute;
  inset: 0;
  transition: opacity 180ms var(--ease-out);
}

/* During a cold variant load the previous proposal must not stand in for the
   requested one. Hide every WebGL/transition canvas until the replacement is
   ready; the stage background remains visible beneath the loading indicator. */
.model-stage.is-model-loading canvas {
  opacity: 0;
}

.model-stage__transition {
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}
.model-stage__transition.is-visible {
  opacity: 1;
  visibility: visible;
}
.model-stage__transition.is-visible.is-fading {
  opacity: 0;
  transition: opacity 320ms cubic-bezier(0.22, 1, 0.36, 1);
}

.model-stage__loading,
.model-stage__error {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  text-align: center;
  padding: var(--s-6);
  font-size: var(--fs-small);
  letter-spacing: var(--ls-nav);
  text-transform: uppercase;
  color: var(--c-ink-3);
  transition: opacity var(--dur-base) var(--ease-out);
  z-index: 4;
}

.model-stage__loading.is-hidden { opacity: 0; pointer-events: none; }

/* The error box is a sibling of the loading box, but is toggled with the
   HTML `hidden` attribute rather than the .is-hidden class above. Because
   the shared `display: flex` rule has the same specificity as the global
   `[hidden]` rule and comes later, an attribute-qualified selector is used
   here so the hidden state wins through a narrowly scoped selector. */
.model-stage__error[hidden] { display: none; }

.model-stage__spinner {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 2px solid var(--c-line-strong);
  border-top-color: var(--c-accent);
  animation: spin 0.9s linear infinite;
}

.model-stage__error { color: var(--warning, #ffc46b); }
.model-stage__error span { max-width: 34ch; }

@keyframes spin { to { transform: rotate(360deg); } }

.model-stage__hint {
  position: absolute;
  left: 50%;
  bottom: var(--s-5);
  transform: translateX(-50%);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-ink-4);
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.model-stage__hint.is-hidden { opacity: 0; }

/* Icon buttons on the right of the 3D stage. These are anchored to the
   stage itself (not the viewport) so they travel with the 3D panel as the
   page scrolls, instead of floating over later sections. Laid out as a
   single column — view presets first, then the action pair; inset from the
   stage's own right edge keeps them clear of the fixed .side-nav, which
   owns the viewport's right edge and never moves. */
.model-stage__tools {
  position: absolute;
  right: var(--s-6);
  top: 50%;
  transform: translateY(-50%);
  z-index: var(--z-nav);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.ghost-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--c-line);
  background: rgba(11, 17, 24, 0.66);
  color: var(--c-ink-3);
  border-radius: var(--r-pill);
  backdrop-filter: blur(4px);
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.ghost-btn__icon {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ghost-btn:hover { color: var(--c-ink); border-color: var(--c-line-strong); }
.ghost-btn[data-state="off"] { color: var(--c-ink-4); }
.ghost-btn[data-state="on"] {
  color: var(--c-accent);
  border-color: var(--c-accent);
}

/* ----------------------- variant switcher ----------------------- */
/* A proposal can ship several variants (up to 7). A compact [ ‹ | 3 / 7 | › ]
   cluster sits on the hero's bottom centre line — the slot the retired
   "Scroll" cue used to occupy — so it is page-centred and clear of both the
   copy column on the left and the tool rail on the right. The whole control
   is invisible at rest and only fades in while the pointer is anywhere over
   the hero, or while the control itself holds *keyboard* focus, so the opening
   frame reads as product-only until the user reaches for it. Touch devices
   have no hover, so there it simply stays visible. */
.overview__variants {
  position: absolute;
  left: 50%;
  bottom: var(--s-6);
  transform: translateX(-50%);
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  gap: var(--s-3);
  /* Only the arrows take pointer events, so drag-rotate across the model and
     the copy underneath is unaffected. */
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
/* Reveal on hover only where hover is a real capability; on touch screens
   :hover sticks after a tap, so gate it. */
@media (hover: hover) and (pointer: fine) {
  .overview:hover .overview__variants { opacity: 1; }
  /* Keyboard users must be able to reach the arrows without hovering. We key
     off :focus-visible rather than :focus-within: a mouse click also focuses a
     button, and that focus lingers after the pointer has moved away — which
     would leave the cluster permanently on screen. */
  .overview__variants:has(:focus-visible) { opacity: 1; }
}
@media not all and (hover: hover) {
  .overview__variants { opacity: 1; }
}

.variant-arrow {
  flex: none;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--c-line);
  background: rgba(11, 17, 24, 0.66);
  color: var(--c-ink-3);
  border-radius: var(--r-pill);
  backdrop-filter: blur(4px);
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.variant-arrow:hover { color: var(--c-ink); border-color: var(--c-line-strong); }
.variant-arrow:disabled { opacity: 0.3; cursor: default; }
.variant-arrow__icon {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Centred between the arrows; the fixed 44px box keeps "3 / 7" on the same
   optical baseline as the pills rather than floating against them. */
.variant-counter {
  flex: none;
  min-width: 46px;
  text-align: center;
  pointer-events: none;
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-ink-4);
}

/* ---------------------------- hero copy ---------------------------- */
.overview__copy {
  position: relative;
  z-index: var(--z-content);
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  /* Copy lives in a left column; the right column is reserved for the 3D
     product so the two never collide. */
  display: grid;
  grid-template-columns: minmax(0, 46%) 1fr;
  grid-template-areas:
    "kicker   ."
    "title    ."
    "lead     dimension";
  align-items: end;
  column-gap: var(--s-6);
  /* The copy sits above the 3D stage, and its box spans the full hero
     including the model area. Pointer events must fall through to the
     canvas underneath so drag-rotate and wheel-zoom reach OrbitControls;
     the copy itself holds no interactive controls. */
  pointer-events: none;
}

.overview__kicker {
  grid-area: kicker;
  margin-bottom: var(--s-3);
}

.overview__title {
  grid-area: title;
  font-size: var(--fs-hero);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-hero);
  font-weight: var(--fw-light);
  max-width: 12ch;
  background: linear-gradient(180deg, var(--c-ink) 0%, var(--c-ink-2) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.overview__title span { display: block; }
.overview__title span:nth-child(2) {
  color: transparent;
  background: linear-gradient(180deg, var(--c-ink-2) 0%, var(--c-ink-4) 100%);
  -webkit-background-clip: text;
  background-clip: text;
}

/* The copy column is only 46% of the hero, and --fs-hero is tuned for a
   two-glyph CJK line like "感知". An English word such as "Collaborate" is
   many times wider, so at the CJK size it overruns the column and slides
   under the 3D model. The English title therefore needs its own clamp: sized
   against the same 46% column instead of the full hero, it lands at roughly
   two-thirds of the CJK size, which keeps it the same optical weight as the
   Chinese / Japanese titles rather than shrinking it to a caption. The
   longest word fills ~94% of the column at every width from 1181px up, so it
   reads large but never collides with the model. The 8.5rem cap takes over
   once --content-max pins the column at 1560px. */
html[lang="en"] .overview__title {
  font-size: clamp(2.5rem, 7.8vw, 8.5rem);
}

/* Proposal-led headlines carry more meaning than the original two-character
   labels. Scale only those longer lines down so they keep a clean left column
   while short proposal titles retain their cinematic impact. */
.overview__title.is-long-title {
  /* Keep the full, 11-character proposal headline inside the left hero
     column. The previous 3.55vw size clipped the final characters on common
     desktop widths because this title is deliberately kept on one line. */
  font-size: clamp(2rem, 3vw, 3.25rem);
  line-height: 1.08;
  max-width: none;
}

.overview__title.is-long-title span {
  /* Let long headline lines wrap inside the available hero column. Keeping
     these spans nowrap made the end of the slogan disappear on narrower
     screens because the overview clips its contents. */
  white-space: normal;
  text-wrap: balance;
}

html[lang="en"] .overview__title.is-long-title {
  font-size: clamp(1.65rem, 2.65vw, 3rem);
  max-width: 14ch;
}

html[lang="en"] .overview__title.is-long-title span {
  white-space: normal;
}

.overview__lead {
  grid-area: lead;
  max-width: 42ch;
  color: var(--c-ink-2);
  font-size: var(--fs-lead);
  line-height: 1.62;
  margin-top: var(--s-4);
}

.overview__dimension {
  grid-area: dimension;
  align-self: end;
  justify-self: end;
  text-align: right;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding-left: var(--s-5);
  border-left: 1px solid var(--c-line);
}

.overview__dimension-label {
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-ink-4);
}

.overview__dimension-value {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--c-ink-2);
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* ---------------------------- scroll hint ---------------------------- */
/* Retired: the slot it occupied along the bottom edge now hosts the variant
   arrows, which need the width more than a decorative "Scroll" cue did. */

/* A slow, shallow bob so the collapsed nav handle reads as "alive" without
   pulling the eye. Kept off the opening/closing transition by pausing it
   whenever the list is unfurled. */
@keyframes nav-float {
  0%, 100% { transform: translateY(-3px); }
  50% { transform: translateY(3px); }
}

/* ----------------------------- story sections ----------------------------- */
/* Pinned-section pattern (requirement 1): each section is a tall "track".
   Its inner .story-section__sticky holds the viewport still while the
   content points slide past, so the page itself never appears to move. */
.story-section {
  position: relative;
  background: var(--c-void);
}

.story-section--details {
  background: var(--c-void);
}

/* --------------------------- curtain beats --------------------------- */
/* A plain-ground breath between two pinned chapters. It carries one short
   line and nothing else — no number, no title — so the eye rests without
   leaving the dark space. The line's opacity is written per-frame by
   updateCurtains() as --curtain-o, peaking when the beat is centred. */
.curtain {
  position: relative;
  z-index: 2;
  isolation: isolate;
  min-height: 100svh;
  margin-block: calc(var(--section-overlap) * -1);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--section-overlap) + var(--gutter)) var(--gutter);
}

.curtain::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
    transparent 0%,
    var(--c-void) var(--section-overlap),
    var(--c-void) calc(100% - var(--section-overlap)),
    transparent 100%);
}

.curtain__line {
  position: relative;
  z-index: 1;
  max-width: 30ch;
  margin: 0;
  text-align: center;
  font-size: clamp(1.5rem, 3.2vw, 2.6rem);
  font-weight: 350;
  line-height: 1.35;
  letter-spacing: -0.01em;
  text-wrap: balance;
  color: var(--c-ink);
  opacity: var(--curtain-o, 0);
  transform: translateY(calc((1 - var(--curtain-o, 0)) * 8px));
  will-change: opacity, transform;
}

.story-section__sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  flex-direction: column;
  overflow: clip;
  background: inherit;
}

/* Note: there is intentionally no .section-head rule set any more. Every
   story section is a full-bleed image + copy stack whose image already
   carries its own headline type, so a block-level eyebrow + title only
   duplicated it. */

/* Content point stack — advanced by the scroll driver (see "pin tracks"). */
.story-shots,
.detail-shots,
.scenario-shots {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  /* None of the three story sections render a .section-head — each shot
     image already carries its own headline type, so a block-level eyebrow +
     title duplicated it. With the header gone the shots run full-bleed to
     the top of the viewport; this variable is what keeps their copy clear
     of the fixed site header. */
  --shot-top-safe: calc(var(--header-h) + var(--s-9));
}

/* ---------------------------- pin dots rail ---------------------------- */
/* A jump control for the pinned sections. Scrolling is the primary way to
   move through the shots, but a client often wants to land on one specific
   detail; the rail turns that into a single click.

   Anchored to .story-section__sticky (not the section) on purpose: the
   section is the full, very tall track, so a bottom offset against it would
   sit at the end of the whole track. The sticky element is exactly one
   viewport tall and is what stays on screen, so anchoring there keeps the
   rail pinned to the bottom of the visible area the whole way through. */
.pin-dots {
  position: absolute;
  left: 50%;
  bottom: var(--s-6);
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  /* No capsule/plate behind the rail: the marks float directly on the imagery
     so nothing breaks the edge-to-edge composition. Legibility is carried by
     the mark contrast + a soft drop shadow instead of a panel. */
  filter: drop-shadow(0 1px 6px rgba(0, 0, 0, 0.55));
}

.pin-dots__dot {
  appearance: none;
  border: 0;
  padding: 0;
  margin: 0;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  background: transparent;
  cursor: pointer;
  /* The hit area is generous while the visible mark stays small, so the dots
     are easy to click without dominating the frame. */
  border-radius: 50%;
}

.pin-dots__bar {
  display: block;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.34);
  transition: width 0.32s var(--ease-out), background-color 0.32s var(--ease-out),
    box-shadow 0.32s var(--ease-out);
}

.pin-dots__dot:hover .pin-dots__bar {
  background: rgba(255, 255, 255, 0.6);
}

/* The active dot stretches into a bar and picks up the same cyan signature
   used by the model's light strip, so the rail reads as part of the product. */
.pin-dots__dot.is-active .pin-dots__bar {
  width: 22px;
  background: #1e9fe0;
  box-shadow: 0 0 10px rgba(30, 159, 224, 0.55);
}

.pin-dots__dot:focus-visible {
  outline: 2px solid #1e9fe0;
  outline-offset: 2px;
}

/* Numeric progress beside the rail. Tabular figures keep the read-out from
   shifting as the count ticks over. */
.pin-dots__count {
  margin-left: var(--s-2);
  padding-left: var(--s-3);
  border-left: 1px solid rgba(255, 255, 255, 0.16);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-label);
  color: rgba(255, 255, 255, 0.72);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Story + scenario shots share one treatment: absolutely stacked points,
   seam-blended into the page so imagery reads edge-to-edge with no frame.

   These are NOT driven by a class-based transition. The scroll driver writes
   a continuous 0..1 --shot-o value per point on every frame, and everything
   visual is derived from it, so a change tracks the scroll position exactly
   instead of firing a fixed-duration tween that can lag behind the wheel.
   --shot-o, --shot-shift and --shot-scale are seeded by JS before first
   paint, so the defaults here are only a safety net. */
.story-shot,
.scenario-shot {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  align-items: end;
  overflow: hidden;
  opacity: var(--shot-o, 0);
  /* Keep a point interactive only while it is actually showing; without this
     the stacked layers swallow pointer events for the ones behind them. */
  pointer-events: none;
  transform: translateY(var(--shot-shift, 8%));
  filter: blur(var(--shot-blur, 3px));
  /* NO transition here. The scroll driver writes --shot-blur on every frame;
     a CSS transition would re-ease each new value over its own fixed duration
     and lag behind the wheel, desyncing blur from opacity/transform and
     reading as stutter. Every property must track the scroll value directly. */
  will-change: opacity, transform, filter;
}

.story-shot.is-active,
.scenario-shot.is-active {
  pointer-events: auto;
}

.story-shot__media,
.scenario-shot__media {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  overflow: hidden;
}

.story-shot__media img,
.scenario-shot__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(var(--shot-scale, 1.06));
}

/* Seam blending (requirement 2): top + bottom atmosphere wash the image
   into the surrounding ground bands so no hard image border is visible. */
.story-shot__media::after,
.scenario-shot__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--seam-t), var(--seam-b), var(--seam-l), var(--seam-r);
  opacity: 1;
}

.story-shot__content,
.scenario-shot__content {
  position: relative;
  z-index: var(--z-content);
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--shot-top-safe, var(--s-10)) var(--gutter)
    calc(var(--s-8) + var(--jump-bar-h));
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.story-shot[data-placement^="top-"] .story-shot__content,
.scenario-shot[data-placement^="top-"] .scenario-shot__content { justify-content: flex-start; }
.story-shot[data-placement="top-center"],
.scenario-shot[data-placement="top-center"],
.detail-shot[data-placement="top-center"] { align-items: start; }
.story-shot[data-placement$="-center"] .story-shot__content,
.scenario-shot[data-placement$="-center"] .scenario-shot__content {
  align-items: center;
  text-align: center;
}

.story-shot__no,
.scenario-shot__no {
  font-family: var(--font-mono);
  font-size: var(--fs-dim);
  color: var(--c-accent);
  letter-spacing: var(--ls-label);
  margin-bottom: var(--s-2);
}

.story-shot__title,
.scenario-shot__title {
  font-size: clamp(1.9rem, 3.6vw, 3.4rem);
  font-weight: var(--fw-light);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-display);
  max-width: none;
  white-space: nowrap;
  /* Japanese headings wrap by character, and the default greedy wrapping can
     strand a single kana on the last line (e.g. "対話するフロント" breaking as
     "対話するフロン" / "ト"). Balancing splits a wrapped title into even lines
     instead, so a lone final character can't happen. */
  text-wrap: balance;
  margin-bottom: var(--s-4);
  background: linear-gradient(180deg, var(--c-ink) 0%, var(--c-ink-3) 120%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.story-shot__desc,
.scenario-shot__desc {
  max-width: 48ch;
  color: var(--c-ink-2);
  font-size: var(--fs-lead);
  line-height: 1.6;
}

html[lang="en"] .story-shot__title,
html[lang="en"] .scenario-shot__title,
html[lang="en"] .detail-shot__title {
  font-size: clamp(1.5rem, 2.6vw, 2.6rem);
  line-height: 1.16;
  padding-bottom: 0.1em;
}

/* ------------------------------ detail shots ------------------------------ */
/* Fully integrated image + copy (requirement 2): the image is edge-to-edge
   and the text sits directly on an asymmetric directional wash instead of
   in a framed side-by-side card — no visible media boundary. */
.detail-shot {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  align-items: end;
  overflow: hidden;
  opacity: var(--shot-o, 0);
  pointer-events: none;
  transform: translateY(var(--shot-shift, 8%));
  filter: blur(var(--shot-blur, 3px));
  /* No transition: --shot-blur is written by the scroll driver every frame, so
     the value must follow the scroll directly (see the story-shot note). */
  will-change: opacity, transform, filter;
}

.detail-shot.is-active {
  pointer-events: auto;
}

.detail-shot__media {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  overflow: hidden;
}

/* Proposal B's full-product renders use a black lower chassis on a black
   stage. A neutral linear gradient sits BEHIND the transparent render: the
   middle-lower band is lifted just enough to separate the chassis, while the
   top and bottom return to the shared black ground so section seams stay
   invisible. Nothing is layered over the product itself. */
.story-shot[data-backdrop="product-separation"] .story-shot__media,
.detail-shot[data-backdrop="product-separation"] .detail-shot__media,
.scenario-shot[data-backdrop="product-separation"] .scenario-shot__media {
  background: linear-gradient(180deg,
    var(--c-void) 0%,
    var(--c-void) 46%,
    #25292d 70%,
    #171b1f 84%,
    var(--c-void) 100%);
}

.detail-shot__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(var(--shot-scale, 1.05));
  transition: object-position var(--dur-cinematic) var(--ease-out);
}

.detail-shot__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrim-l), var(--seam-t), var(--seam-b), var(--seam-r);
  opacity: 0.52;
}

.detail-shot__content {
  position: relative;
  z-index: var(--z-content);
  width: 100%;
  max-width: var(--content-max);
  min-height: 100%;
  margin: 0 auto;
  padding: var(--shot-top-safe, var(--s-11)) var(--gutter)
    calc(var(--s-9) + var(--jump-bar-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.detail-shot[data-copy-position="top"] .detail-shot__content { justify-content: flex-start; }
.detail-shot[data-copy-position="bottom"] .detail-shot__content {
  justify-content: flex-end;
  padding-bottom: calc(var(--s-10) + var(--jump-bar-h));
}

.detail-shot__no {
  font-family: var(--font-mono);
  font-size: var(--fs-dim);
  color: var(--c-accent);
  letter-spacing: var(--ls-label);
  margin-bottom: var(--s-2);
}

.detail-shot__title {
  font-size: clamp(1.9rem, 3.6vw, 3.4rem);
  font-weight: var(--fw-light);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-display);
  max-width: none;
  white-space: nowrap;
  text-wrap: balance;
  margin-bottom: var(--s-4);
  background: linear-gradient(180deg, var(--c-ink) 0%, var(--c-ink-3) 130%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.detail-shot__text {
  color: var(--c-ink-2);
  font-size: var(--fs-lead);
  line-height: 1.62;
  /* Keep Chinese copy near a 20-character measure at any desktop size. */
  max-width: min(20em, 100%);
}

/* Composition-aware media treatment shared by all three product chapters.
   The image moves into the space opposite the copy, while the wash is local
   to the copy zone and reaches full transparency before the product subject.
   No full-frame dark overlay remains on desktop. */
.story-shot[data-placement$="-left"] .story-shot__media img,
.scenario-shot[data-placement$="-left"] .scenario-shot__media img,
.detail-shot[data-placement$="-left"] .detail-shot__media img {
  object-position: 78% 50%;
  transform: translateX(12%) scale(var(--shot-scale, 1.06));
}
.story-shot[data-placement$="-right"] .story-shot__media img,
.scenario-shot[data-placement$="-right"] .scenario-shot__media img,
.detail-shot[data-placement$="-right"] .detail-shot__media img {
  object-position: 22% 50%;
  transform: translateX(-12%) scale(var(--shot-scale, 1.06));
}
.story-shot[data-placement="top-left"] .story-shot__media img,
.scenario-shot[data-placement="top-left"] .scenario-shot__media img,
.detail-shot[data-placement="top-left"] .detail-shot__media img,
.story-shot[data-placement="top-right"] .story-shot__media img,
.scenario-shot[data-placement="top-right"] .scenario-shot__media img,
.detail-shot[data-placement="top-right"] .detail-shot__media img {
  object-position-y: 68%;
}
.story-shot[data-placement="bottom-left"] .story-shot__media img,
.scenario-shot[data-placement="bottom-left"] .scenario-shot__media img,
.detail-shot[data-placement="bottom-left"] .detail-shot__media img,
.story-shot[data-placement="bottom-right"] .story-shot__media img,
.scenario-shot[data-placement="bottom-right"] .scenario-shot__media img,
.detail-shot[data-placement="bottom-right"] .detail-shot__media img {
  object-position-y: 32%;
}
.story-shot[data-placement="top-center"] .story-shot__media img,
.scenario-shot[data-placement="top-center"] .scenario-shot__media img,
.detail-shot[data-placement="top-center"] .detail-shot__media img {
  object-position: 50% 72%;
}
.story-shot[data-placement="bottom-center"] .story-shot__media img,
.scenario-shot[data-placement="bottom-center"] .scenario-shot__media img,
.detail-shot[data-placement="bottom-center"] .detail-shot__media img {
  object-position: 50% 28%;
}

/* The multi-level detail is about the repeated interfaces along the tower.
   Crop out the low-contrast chassis and let the upper rack occupy the frame. */
.detail-shot[data-composition="upper-frame"] .detail-shot__media img {
  object-position: 50% 50%;
  transform: translate(-18%, 15%) scale(1.5);
}

.story-shot[data-placement$="-left"] .story-shot__media::after,
.scenario-shot[data-placement$="-left"] .scenario-shot__media::after,
.detail-shot[data-placement$="-left"] .detail-shot__media::after {
  opacity: 1;
  background: var(--scrim-l), var(--seam-t), var(--seam-b), var(--seam-r);
}
.story-shot[data-placement$="-right"] .story-shot__media::after,
.scenario-shot[data-placement$="-right"] .scenario-shot__media::after,
.detail-shot[data-placement$="-right"] .detail-shot__media::after {
  opacity: 1;
  background: var(--scrim-r), var(--seam-t), var(--seam-b), var(--seam-l);
}
.story-shot[data-placement="top-center"] .story-shot__media::after,
.scenario-shot[data-placement="top-center"] .scenario-shot__media::after,
.detail-shot[data-placement="top-center"] .detail-shot__media::after {
  opacity: 1;
  background: linear-gradient(180deg,
    rgba(4, 7, 11, 0.82) 0%,
    rgba(4, 7, 11, 0.58) 10%,
    rgba(4, 7, 11, 0.24) 20%,
    rgba(4, 7, 11, 0) 30%),
    var(--seam-b), var(--seam-l), var(--seam-r);
}
.story-shot[data-placement="bottom-center"] .story-shot__media::after,
.scenario-shot[data-placement="bottom-center"] .scenario-shot__media::after,
.detail-shot[data-placement="bottom-center"] .detail-shot__media::after {
  opacity: 1;
  background: linear-gradient(0deg,
    rgba(4, 7, 11, 0.84) 0%,
    rgba(4, 7, 11, 0.58) 24%,
    rgba(4, 7, 11, 0.26) 48%,
    rgba(4, 7, 11, 0.08) 64%,
    rgba(4, 7, 11, 0) 78%),
    var(--seam-t), var(--seam-l), var(--seam-r);
}

/* The montage shots already contain generous black space above their subjects.
   Keep them at full frame size and place copy in that upper band; shrinking the
   entire montage made the products look like thumbnails and put copy over them. */
.story-shot[data-placement="top-center"] .story-shot__media img,
.scenario-shot[data-placement="top-center"] .scenario-shot__media img,
.detail-shot[data-placement="top-center"] .detail-shot__media img {
  transform: scale(var(--shot-scale, 1.06));
}
.story-shot[data-placement="bottom-center"] .story-shot__media img,
.scenario-shot[data-placement="bottom-center"] .scenario-shot__media img,
.detail-shot[data-placement="bottom-center"] .detail-shot__media img {
  transform: translateY(-18%) scale(0.9);
}
.story-shot[data-placement="top-center"] .story-shot__content,
.scenario-shot[data-placement="top-center"] .scenario-shot__content,
.detail-shot[data-placement="top-center"] .detail-shot__content {
  min-height: 0;
  height: 38%;
  padding-top: clamp(3.5rem, 8vh, 7rem);
}
.story-shot[data-placement$="-left"] .story-shot__content,
.scenario-shot[data-placement$="-left"] .scenario-shot__content,
.detail-shot[data-placement$="-left"] .detail-shot__content,
.story-shot[data-placement$="-right"] .story-shot__content,
.scenario-shot[data-placement$="-right"] .scenario-shot__content,
.detail-shot[data-placement$="-right"] .detail-shot__content {
  width: min(52%, 860px);
  margin-left: var(--gutter);
  margin-right: var(--gutter);
}
.story-shot[data-placement$="-right"] .story-shot__content,
.scenario-shot[data-placement$="-right"] .scenario-shot__content,
.detail-shot[data-placement$="-right"] .detail-shot__content {
  justify-self: end;
  align-items: flex-start;
  text-align: left;
}

/* When the render has shifted left or right to leave a copy lane, use that
   full-height lane and centre its text vertically instead of pinning it to a
   corner. Text remains left-aligned on either side. */
.story-shot[data-placement$="-left"] .story-shot__content,
.scenario-shot[data-placement$="-left"] .scenario-shot__content,
.story-shot[data-placement$="-right"] .story-shot__content,
.scenario-shot[data-placement$="-right"] .scenario-shot__content {
  justify-content: center;
}

/* Keep headings and long descriptions inside their copy column. The former
   nowrap headings could extend across a centred render even when the body
   copy had been narrowed to leave the product clear. */
.story-shot[data-placement$="-left"] .story-shot__title,
.scenario-shot[data-placement$="-left"] .scenario-shot__title,
.detail-shot[data-placement$="-left"] .detail-shot__title,
.story-shot[data-placement$="-right"] .story-shot__title,
.scenario-shot[data-placement$="-right"] .scenario-shot__title,
.detail-shot[data-placement$="-right"] .detail-shot__title {
  max-width: 100%;
  white-space: normal;
  text-wrap: balance;
}
.story-shot[data-placement$="-left"] .story-shot__desc,
.scenario-shot[data-placement$="-left"] .scenario-shot__desc,
.story-shot[data-placement$="-right"] .story-shot__desc,
.scenario-shot[data-placement$="-right"] .scenario-shot__desc {
  max-width: min(32vw, 560px);
}
/* ------------------------------ scenario shots ------------------------------ */
.scenario-shots {
  display: block;
}

/* -------------------------------- footer -------------------------------- */
/* Curtain call. The product line-up fills the viewport as one continuous
   shot: the render is the ground, feathered into the void on every side,
   and the wordmark and copy sit inside it, low, on the dark floor. */
.site-footer {
  position: relative;
  min-height: var(--section-min-h);
  min-height: var(--section-min-h-svh);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  overflow: hidden;
  padding: var(--s-9) var(--gutter) var(--s-9);
  background: var(--c-void);
}

.site-footer__media {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  background:
    radial-gradient(ellipse 80% 70% at 50% 42%, rgba(30, 44, 64, 0.5) 0%, rgba(4, 7, 11, 0) 70%),
    var(--c-void);
}

.site-footer__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 8%;
  transform: scale(1.02);
  opacity: 0;
  transition: opacity var(--dur-cinematic) var(--ease-out),
              transform var(--dur-epic) var(--ease-out);
  will-change: opacity, transform;
}

/* Feather the shot into the void. The bottom band is pulled down across
   the full width so the centred wordmark and copy rest on a dark pool
   that still belongs to the image's own floor. */
.site-footer__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(0deg, var(--c-void) 0%, rgba(4, 7, 11, 0.97) 12%, rgba(4, 7, 11, 0.78) 26%, rgba(4, 7, 11, 0.34) 44%, rgba(4, 7, 11, 0) 60%),
    linear-gradient(180deg, var(--c-void) 0%, rgba(4, 7, 11, 0.92) 12%, rgba(4, 7, 11, 0.68) 27%, rgba(4, 7, 11, 0.3) 42%, rgba(4, 7, 11, 0) 58%),
    linear-gradient(90deg, rgba(4, 7, 11, 0.6) 0%, rgba(4, 7, 11, 0) 18%, rgba(4, 7, 11, 0) 82%, rgba(4, 7, 11, 0.6) 100%);
}

.site-footer__inner {
  position: relative;
  z-index: var(--z-content);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 60ch;
}

.site-footer__brand {
  font-size: clamp(1.5rem, 2.4vw, 2.1rem);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-label);
  color: var(--c-ink);
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-slow) var(--ease-out) 0.35s,
              transform var(--dur-slow) var(--ease-out) 0.35s;
}

.site-footer__rule {
  width: 34px;
  height: 1px;
  margin: var(--s-4) 0 var(--s-4);
  background: var(--c-accent-line);
  transform: scaleX(0);
  transform-origin: 50% 50%;
  transition: transform var(--dur-slow) var(--ease-out) 0.5s;
}

.site-footer__line {
  margin: 0 auto;
  max-width: 34ch;
  color: var(--c-ink-2);
  font-size: var(--fs-lead);
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-out) 0.55s,
              transform var(--dur-slow) var(--ease-out) 0.55s;
}

.site-footer__note {
  margin-top: var(--s-5);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-ink-3);
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-out) 0.65s,
              transform var(--dur-slow) var(--ease-out) 0.65s;
}

.site-footer__top {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--s-7);
  padding: 11px 20px;
  border: var(--hairline);
  border-radius: var(--r-pill);
  background: rgba(16, 24, 34, 0.5);
  backdrop-filter: blur(10px);
  color: var(--c-ink-3);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-nav);
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--dur-slow) var(--ease-out) 0.72s,
              transform var(--dur-slow) var(--ease-out) 0.72s,
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}

.site-footer__top svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.site-footer__top:hover {
  color: var(--c-ink);
  border-color: var(--c-line-strong);
  background: var(--c-surface-2);
}

/* Curtain rises: the shot eases in from a slow push-out and the type
   settles low over it. */
.site-footer.is-visible .site-footer__media img {
  opacity: 1;
  transform: scale(1);
}

.site-footer.is-visible .site-footer__brand,
.site-footer.is-visible .site-footer__line,
.site-footer.is-visible .site-footer__note,
.site-footer.is-visible .site-footer__top {
  opacity: 1;
  transform: translateY(0);
}

.site-footer.is-visible .site-footer__rule { transform: scaleX(1); }

/* ------------------------------- reveal ------------------------------- */
.reveal {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* ----------------------------- scroll-driven animations ----------------------------- */
/* The hero model stage and title used to fade/scale in with scroll-driven
   animations pinned to scroll progress. Because they filled "both" directions
   they sat frozen at their start state at the top of the page: the stage at
   opacity 0.55 (making the 3D model look like it had a dark film over it) and
   the title at opacity 0 (hiding the hero copy). Both the model and the hero
   text are now permanent, so these animations are removed entirely. */

/* ----------------------------- responsive ----------------------------- */
@media (max-width: 1180px) {
  .overview__copy {
    grid-template-columns: 1fr;
    grid-template-areas:
      "kicker"
      "title"
      "lead"
      "dimension";
    gap: var(--s-4);
  }
  .overview__dimension {
    justify-self: start;
    text-align: left;
    padding-left: 0;
    border-left: 0;
    padding-top: var(--s-4);
    border-top: 1px solid var(--c-line);
  }
  .overview__title { max-width: 16ch; }
  .model-stage { width: 100%; height: 100%; }
  .side-nav { display: none; }
}

@media (max-width: 900px) {
  /* --jump-bar-h is the exact footprint of the floating .mobile-jump pill
     measured from the viewport bottom (pill height + the gap it floats above
     the edge + the home-indicator inset). Shot copy that bottoms out against
     the viewport reserves this much space so the last line is never hidden
     behind the pill on a short screen. */
  :root {
    --header-h: 62px;
    --jump-bar-h: calc(65px + env(safe-area-inset-bottom));
  }

  /* Left / centre / right header — logo on the left, proposal-switch on the
     centre axis, language control on the right. This is the same arrangement
     desktop uses: the brand sits in normal flow at the left edge, the language
     control sits in normal flow at the right edge, and the proposal-switch is
     lifted onto the centre axis with absolute positioning (see the base rule).
     The phone only needs to pin the two in-flow items to the edges; the switch
     is already out of the flow and centred, so it needs no mobile override. */
  .site-header {
    justify-content: space-between;
  }

  /* The 24px desktop logo is sized against the taller desktop header. On a
     phone the header shrinks to 62px, so the same mark reads oversized and
     competes with the proposal/lang controls. Scale it down proportionally. */
  .brand__logo {
    height: 19px;
  }

  /* The header is crowded on a phone. The desktop 18px side padding, combined
     with the uppercase + letter-spacing treatment, made "PROPOSAL A" far wider
     than the phone can spare — the label must stay spelled out in full, so
     instead of abbreviating we drop the uppercase + tracking on mobile (the
     label still reads as a nav control) and tighten the padding. This keeps
     every language's label on one line. */
  .proposal-switch {
    gap: 0;
    padding: 2px;
  }

  .proposal-switch button {
    padding: 6px 9px;
    font-size: 11.5px;
    letter-spacing: 0.01em;
    text-transform: none;
    white-space: nowrap;
  }

  /* English is the widest language: even tracking-free, "Proposal A" +
     "Proposal B" is ~150px, which cannot sit on the exact centre axis on the
     narrowest phones without lapping the logo on the left. Chinese and Japanese
     are narrow enough to keep the strict centre (the base rule handles them).
     For English only, on phones, we let the switch fall back into the flex row
     so `justify-content: space-between` seats it in the space left between the
     logo and the language control — i.e. still central, just not on the exact
     axis when the screen is too tight. The brand and language control are
     locked against shrinking so the logo keeps its size. */
  html[lang="en"] .site-header {
    gap: 0;
  }

  html[lang="en"] .brand,
  html[lang="en"] .lang-switch {
    flex-shrink: 0;
  }

  html[lang="en"] .proposal-switch {
    position: static;
    transform: none;
  }

  html[lang="en"] .proposal-switch button {
    padding: 6px 7px;
    letter-spacing: 0;
    font-size: 11px;
  }

  /* On the very narrowest phones even the tracking-free English pill only just
     fits, and the centred Chinese/Japanese switch starts to lap the logo. Claw
     back side gutter and a little logo height so all three controls keep a
     visible gap between them. */
  @media (max-width: 380px) {
    .brand__logo {
      height: 16px;
    }

    html[lang="en"] .site-header {
      padding: 0 14px;
    }
  }

  .lang-switch__current,
  .lang-switch__caret { display: none; }
  .lang-switch__trigger { padding: 7px 10px; }

  /* Mobile hero: a vertical stack instead of the desktop overlay.
     copy (top) -> 3D model (centre, the star) -> horizontal tools (bottom).
     The stage is taken out of absolute/overlay positioning so it occupies
     real space and can never pile on top of the copy. */
  .overview {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    text-align: center;
    gap: var(--s-2);
    min-height: 100svh;
    height: 100svh;
    max-height: 100svh;
    /* Bottom padding reserves room for the floating .mobile-jump pill plus a
       little clearance, so the spec line that closes the hero never sits flush
       against the pill. */
    padding: calc(var(--header-h) + var(--s-3)) var(--gutter)
      calc(var(--jump-bar-h) + var(--s-3));
  }

  /* Dissolve the copy wrapper so its four children (kicker, title, lead,
     dimension) become flex items of .overview alongside the stage. That is
     the only way the dimension read-out can be re-ordered to *after* the
     model + tool row — a child cannot escape its own container for ordering.
     Desktop keeps the wrapper as a grid and is untouched. */
  .overview__copy {
    display: contents;
  }

  /* Reading order on mobile: kicker, title, lead, [model + tools], dimension.
     The spec line ends up beneath the control rail, closing the block. */
  .overview__kicker { order: 1; font-size: var(--fs-micro); }
  .overview__title { order: 2; }
  .overview__lead { order: 3; }
  .overview__stage { order: 4; }
  .overview__dimension { order: 5; }

  .overview__stage {
    position: static;
    inset: auto;
    /* basis:0 (not auto) so the stage takes exactly the space left over
       instead of starting from its content height and overflowing. */
    flex: 1 1 0;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--s-2) 0 0;
    min-height: 0;
    overflow: hidden;
  }

  /* The model is the hero of the section: give it all remaining height, and
     put its control rail in normal flow so it sits *under* the model as a
     horizontal row. */
  .model-stage {
    width: 100%;
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }

  .model-stage__tools {
    position: static;
    inset: auto;
    transform: none;
    order: 2;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--s-2);
    padding-top: var(--s-3);
  }

  /* Phones have no hover, so the cluster can never be revealed by pointer.
     Keep it in the hero's bottom band but pin it into normal flow so it
     never lands on the copy, and leave it permanently visible —
     discoverability beats a clean first paint on a screen with no hover. */
  .overview__variants {
    position: static;
    left: auto;
    bottom: auto;
    transform: none;
    justify-content: center;
    gap: var(--s-4);
    padding-top: var(--s-4);
    opacity: 1;
  }
  .variant-arrow {
    width: 38px;
    height: 38px;
  }
  .variant-counter {
    min-width: 0;
  }

  /* The canvas is injected as the first child of .model-stage; let it take
     every pixel above the tool row. Sizing is owned entirely by CSS — the
     viewer no longer writes inline width/height, so stylesheet ownership
     remains explicit. */
  .model-stage canvas { flex: 1 1 auto; min-height: 0; width: 100%; height: 100%; }

  .model-stage__hint { display: none; }

  /* The three key words read as one phrase on mobile (there is no left
     column to clear, so stacking them as three full lines is pure waste).
     Spans are laid inline and separated by a gap instead of a hard line
     break; the desktop version keeps them stacked. */
  .overview__title {
    margin: 0 auto;
    font-size: clamp(2rem, 11vw, 3rem);
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 0 0.5em;
    max-width: none;
  }
  /* English words are far wider than the two-character CJK terms the 11vw
     sizing above was tuned for. At that size "Recognise Collaborate Endure"
     wraps to three tall lines, and because the copy stack shares the hero's
     fixed height with the 3D model, every extra line is taken straight out of
     the model's display area (the model ends up visibly smaller in English
     than in Chinese or Japanese). A smaller, width-proportional size keeps the
     English title to two lines — the same as CJK — so the model gets its space
     back. */
  html[lang="en"] .overview__title {
    font-size: clamp(1.6rem, 8vw, 2.125rem);
  }
  .overview__title.is-long-title,
  html[lang="en"] .overview__title.is-long-title {
    display: block;
    font-size: clamp(1.35rem, 5.4vw, 1.75rem);
    line-height: 1.12;
    max-width: none;
  }
  .overview__title span { display: inline; }
  .overview__title.is-long-title span { display: block; }
  .overview__title.is-long-title span { white-space: normal; }
  html[lang="en"] .overview__title.is-long-title span {
    white-space: normal;
  }
  .overview__lead {
    margin: 0 auto;
    max-width: 38ch;
    font-size: var(--fs-small);
    line-height: 1.5;
    color: var(--c-ink-3);
  }
  .overview__dimension {
    align-self: center;
    justify-self: center;
    flex-direction: row;
    align-items: baseline;
    gap: var(--s-2);
    text-align: center;
    padding-left: 0;
    border-left: 0;
    border-top: 0;
    padding-top: 0;
    /* Extra breathing room above the control rail — the flex gap (--s-2) is
       too tight to read as a separate closing block. */
    margin-top: var(--s-4);
  }

  .story-shot__title,
  .scenario-shot__title,
  .detail-shot__title {
    font-size: clamp(1.35rem, 5.6vw, 1.85rem);
    white-space: nowrap;
  }
  html[lang="en"] .story-shot__title,
  html[lang="en"] .scenario-shot__title,
  html[lang="en"] .detail-shot__title {
    font-size: clamp(0.95rem, 4vw, 1.3rem);
    line-height: 1.18;
    padding-bottom: 0.1em;
  }

  /* On phones every shot returns to one stable bottom-centred layout.
     Neutralise the desktop placement crop and directional alignment so the
     narrower viewport does not produce fragile image/text collisions. */
  .story-shot[data-placement] .story-shot__media img,
  .scenario-shot[data-placement] .scenario-shot__media img,
  .detail-shot[data-placement] .detail-shot__media img {
    object-position: 50% 27%;
    transform: scale(var(--shot-scale, 1.06));
  }
  .story-shot[data-placement] .story-shot__media::after,
  .scenario-shot[data-placement] .scenario-shot__media::after,
  .detail-shot__media::after,
  .detail-shot[data-placement$="-right"] .detail-shot__media::after {
    background: linear-gradient(0deg,
      rgba(4, 7, 11, 0.9) 0%,
      rgba(4, 7, 11, 0.62) 16%,
      rgba(4, 7, 11, 0.1) 34%,
      rgba(4, 7, 11, 0) 44%),
      var(--seam-t), var(--seam-b), var(--seam-l), var(--seam-r);
    opacity: 1;
  }
  .story-shot[data-placement="top-center"] .story-shot__content,
  .scenario-shot[data-placement="top-center"] .scenario-shot__content,
  .detail-shot[data-placement="top-center"] .detail-shot__content {
    height: auto;
    min-height: 100%;
    padding-top: var(--shot-top-safe, var(--s-10));
  }
  .story-shot[data-placement$="-left"] .story-shot__content,
  .scenario-shot[data-placement$="-left"] .scenario-shot__content,
  .story-shot[data-placement$="-right"] .story-shot__content,
  .scenario-shot[data-placement$="-right"] .scenario-shot__content {
    width: 100%;
    max-width: var(--content-max);
    margin-left: auto;
    margin-right: auto;
  }
  .story-shot[data-placement] .story-shot__content,
  .scenario-shot[data-placement] .scenario-shot__content {
    align-items: center;
    text-align: center;
    justify-content: flex-end;
  }
  .story-shot__title,
  .scenario-shot__title { margin-left: auto; margin-right: auto; }
  .story-shot__desc,
  .scenario-shot__desc { margin: 0 auto; }

  .detail-shot__content { align-items: flex-start; text-align: left; }
  .detail-shot__title,
  .detail-shot__text { margin-left: 0; margin-right: 0; }

  /* Centred copy runs nearly edge-to-edge on a phone, so a 2-line paragraph
     left-justified inside it dumps the last 1-2 characters onto their own
     line and the block reads lopsided. Balance the wrap so the lines split
     evenly, and pull the measure in from the gutter for a calmer column. */
  .story-shot__desc,
  .scenario-shot__desc,
  .detail-shot__text {
    max-width: 30ch;
    text-wrap: balance;
  }

  /* Floating pill: instead of an edge-to-edge bar glued to the bottom, the
     nav becomes a translucent capsule that hovers a little above the bottom
     edge and is centred horizontally. It reads as a distinct object, stays in
     the thumb zone, and — sized to its content — covers less of the shot
     underneath. `width: max-content` makes it hug the four labels (a fixed box
     would otherwise shrink-to-fit and clip the last item). Centring uses
     `50vw` (full visual viewport) rather than `50%`/`margin-inline`, so a
     classic scrollbar on the containing block can't pull it off-centre. */
  .mobile-jump {
    display: flex;
    position: fixed;
    left: 50vw;
    bottom: calc(var(--s-4) + env(safe-area-inset-bottom));
    transform: translateX(-50%);
    z-index: var(--z-nav);
    gap: var(--s-1);
    width: max-content;
    max-width: calc(100vw - 2 * var(--gutter));
    padding: var(--s-1);
    background: rgba(10, 16, 24, 0.72);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    backdrop-filter: blur(16px) saturate(140%);
    border: var(--hairline);
    border-radius: var(--r-pill);
    box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.9);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .mobile-jump::-webkit-scrollbar { display: none; }

  .mobile-jump a {
    flex: 0 0 auto;
    padding: 9px 15px;
    border-radius: var(--r-pill);
    font-size: var(--fs-micro);
    letter-spacing: var(--ls-nav);
    text-transform: uppercase;
    color: var(--c-ink-3);
    border: 1px solid transparent;
    transition: color var(--dur-fast) var(--ease-out),
      background var(--dur-fast) var(--ease-out);
  }
  .mobile-jump a.is-active { color: var(--c-accent); }

  .site-footer { padding-bottom: calc(var(--s-9) + 56px); }
}

/* Backdrop blur is expensive on mobile GPUs when several full-width surfaces
   stack (header + menus). Drop it there and compensate with a more opaque
   ground so text contrast is unchanged. The jump pill is small and is the one
   surface that *needs* its translucency to read as floating, so it keeps its
   blur. */
@media (max-width: 900px) {
  .site-header,
  .lang-switch__menu,
  .site-footer__top {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .site-header { background: linear-gradient(180deg, rgba(4, 7, 11, 0.94) 0%, rgba(4, 7, 11, 0.7) 70%, rgba(4, 7, 11, 0) 100%); }
  .lang-switch__menu { background: rgba(10, 15, 22, 0.98); }
  .site-footer__top { background: rgba(16, 24, 34, 0.72); }
}

@media (max-width: 560px) {
  .overview {
    padding-bottom: calc(var(--jump-bar-h) + var(--s-3));
  }
  .overview__title { letter-spacing: -0.03em; }
  .overview__lead { font-size: var(--fs-small); }
  .story-shot__content,
  .scenario-shot__content { padding: var(--shot-top-safe, var(--s-9)) var(--gutter) calc(var(--s-7) + var(--jump-bar-h)); }
  .detail-shot__content { padding: var(--shot-top-safe, var(--s-10)) var(--gutter) calc(var(--s-8) + var(--jump-bar-h)); }
  .story-shot__title,
  .scenario-shot__title,
  .detail-shot__title { font-size: clamp(1.35rem, 5.6vw, 1.85rem); }
}

/* ------------------------- static fallback (no sticky) ------------------------- */
@media (prefers-reduced-motion: reduce) {
  .story-section { display: flex; flex-direction: column; min-height: 100vh; min-height: 100svh; }
  .story-section__sticky { position: static; height: auto; flex: 1 1 auto; }
  .curtain { min-height: 72svh; margin-block: 0; }
  .curtain::before { background: var(--c-void); }
  .curtain__line { --curtain-o: 1; transform: none; }
  .story-shots, .detail-shots, .scenario-shots { flex: 0 0 auto; }
  .story-shot, .detail-shot, .scenario-shot {
    position: relative;
    inset: auto;
    /* The scroll driver writes inline --shot-o per frame, so the reduced-motion
       fallback has to neutralise the variable itself — overriding `opacity`
       alone would lose to the inline custom property. Every point shows
       stacked in normal flow instead. */
    --shot-o: 1;
    --shot-shift: 0%;
    --shot-scale: 1;
    --shot-blur: 0px;
    opacity: 1;
    filter: none;
    pointer-events: auto;
    transform: none;
    min-height: 92vh;
    min-height: 92svh;
  }
  /* The rail is a jump control for the pinned, cross-fading presentation.
     In this fallback there is no pinning — every shot is laid out in normal
     flow — so the dots have nothing to jump between and would just float
     over the stacked content. */
  .pin-dots { display: none; }
}

/* --------------------------- reduced motion --------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { opacity: 1; transform: none; }
  .model-stage__spinner { animation: none; }
  .overview__scanlines { animation: none; }
  .side-nav__capsule { animation: none; }
}
