/* =====================================================================
   NOCTIS v2 — primitives.css
   one file, nine components. No layout, no page chrome — just the
   building blocks every page will import. Pulls colour / type / motion
   tokens from tokens.css (already loaded via base.css @import).
   ===================================================================== */

/* ============================== 0 · section--dark =================
   Brand-wide colour-theme modifier. Re-declares the palette variables
   that the section's own rules already read (--bg-deep etc) — that's
   the *only* thing this class is allowed to do. No position, no
   display, no inset, no anything that touches layout. The section's
   page-specific CSS (.hero, .pass, .map-preview, .login-grid__right)
   keeps owning its layout. */
.section--dark {
  --bg:           oklch(0.10  0.005  60);
  --bg-elev:      oklch(0.14  0.008  60);
  --bg-deep:      oklch(0.075 0.010  55);
  --fg:           oklch(0.92  0.015  75);
  --fg-soft:      oklch(0.76  0.015  70);
  --fg-muted:     oklch(0.55  0.012  70);
  --fg-faint:     oklch(0.40  0.010  68);
  --accent:       oklch(0.72  0.105  68);
  --accent-deep:  oklch(0.60  0.115  60);
  --line:         oklch(0.22  0.005  70);
  --line-soft:    oklch(0.18  0.008  60);
}


/* ============================== 1 · nav ===========================
   floating pill, blur backdrop, three slots (brand / links / tools).
   on phones the link list collapses behind a burger that opens a
   fullscreen <dialog>.
*/
.nav {
  position: fixed;
  top: clamp(0.75rem, 2vw, 1.5rem);
  left: clamp(0.75rem, 2vw, 1.5rem);
  right: clamp(0.75rem, 2vw, 1.5rem);
  z-index: 50;

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 0.55rem 1.1rem;

  font-family: var(--font-sans);
  font-size: 12px;
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--fg-soft);

  background: color-mix(in oklab, var(--bg) 72%, transparent);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border: var(--border);
  border-radius: var(--radius-lg);
  transition:
    background var(--t-base) var(--ease-out),
    border-color var(--t-base) var(--ease-out);
}

.nav__brand {
  font-weight: 500;
  letter-spacing: 0.32em;
  color: var(--fg);
  font-size: 12px;
}

.nav__links {
  display: flex;
  gap: clamp(1rem, 3vw, 2.25rem);
}
.nav__links a {
  position: relative;
  color: var(--fg-soft);
  transition: color var(--t-base) var(--ease-out);
}
.nav__links a::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -6px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-base) var(--ease-out);
}
.nav__links a:hover {
  color: var(--fg);
}
.nav__links a:hover::after,
.nav__links a[aria-current="page"]::after {
  transform: scaleX(1);
}

.nav__tools { display: flex; align-items: center; gap: var(--space-3); }
.nav__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  border-radius: 999px;
  color: var(--fg-soft);
  transition: color var(--t-base) var(--ease-out),
              background var(--t-base) var(--ease-out);
}
.nav__btn:hover { color: var(--fg); background: var(--line-soft); }
.nav__btn[data-lang-toggle] {
  font-weight: 500;
  letter-spacing: 0;
  font-size: 12px;
}
.nav__btn .ic { width: 14px; height: 14px; }

.nav__burger { display: none; color: var(--fg-soft); }
.nav__burger .ic { width: 20px; height: 20px; }

/* /login/ entry — sits between the lang and theme toggle */
.nav__login {
  display: inline-flex;
  align-items: center;
  position: relative;
  margin: 0 var(--space-1);
  padding: 0;
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-soft);
  transition: color var(--t-base) var(--ease-out);
}
.nav__login::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -6px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-base) var(--ease-out);
}
.nav__login:hover {
  color: var(--accent);
}
.nav__login:hover::after,
.nav__login[aria-current="page"]::after { transform: scaleX(1); }

@media (max-width: 720px) {
  .nav { padding: 0.5rem 0.85rem; }
  .nav__links { display: none; }
  .nav__brand { font-size: 11px; }
  .nav__burger { display: inline-flex; }
  .nav__tools .nav__btn[data-lang-toggle] { display: none; }  /* moved into drawer */
}

/* nav drawer — fullscreen dialog for mobile */
.nav__drawer {
  width: 100vw;
  height: 100vh;
  max-width: 100%;
  max-height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--bg);
  color: var(--fg);
  inset: 0;
  opacity: 0;
  transition: opacity var(--t-base) var(--ease-out);
}
.nav__drawer[open] {
  opacity: 1;
  display: flex;
  flex-direction: column;
}
.nav__drawer::backdrop { background: transparent; }
.nav__drawer__inner {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 1.25rem var(--pad-x) 2.5rem;
}
.nav__drawer__top {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.nav__drawer__links {
  display: flex; flex-direction: column;
  gap: var(--space-4);
  font-family: var(--font-serif);
  font-size: clamp(1.75rem, 7vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: -0.01em;
}
[lang^="zh"] .nav__drawer__links { letter-spacing: 0; }
.nav__drawer__links a { color: var(--fg); }
.nav__drawer__links a:hover { color: var(--accent); }


/* ============================== 2 · footer ========================
   single row, brand + location | links. one extra muted line beneath.
*/
.footer {
  padding: var(--space-5) var(--pad-x) var(--space-4);
  background: var(--bg);
  border-top: var(--border);
  font-family: var(--font-sans);
  font-size: var(--type-eyebrow);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.footer__top {
  display: flex;
  justify-content: flex-end;
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-3);
  border-bottom: var(--border);
}
.footer__top a {
  font-family: var(--font-sans);
  font-size: var(--type-eyebrow);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
  transition: color var(--t-base) var(--ease-out);
}
.footer__top a:hover { color: var(--accent); }

.footer__row {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.footer__row a {
  transition: color var(--t-base) var(--ease-out);
}
.footer__row a:hover { color: var(--fg); }
.footer__sub {
  margin-top: var(--space-3);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--fg-faint);
}


/* ============================== 3 · link-arrow ====================
   text + underline + arrow. three sizes — s / m / l. hover slides
   arrow 4-6px right. NEVER a filled rectangle.
*/
.link-arrow {
  display: inline-flex;
  align-items: baseline;
  gap: 0.65em;
  font-family: var(--font-sans);
  font-weight: 400;
  color: var(--fg);
  padding-bottom: 4px;
  position: relative;
  border: 0;
  background: transparent;
  transition: color var(--t-base) var(--ease-out);
}
/* base line — visible by default, retracts toward the right on hover */
.link-arrow::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--line);
  transform-origin: right center;
  transition: transform 200ms var(--ease-out);
}
/* accent line — hidden by default, grows in from the left after the
   base line finishes retracting (180 ms delay = 200 + 180 ≈ 400 ms
   total for the full "right-retract → left-grow" gesture the brief
   asked for). */
.link-arrow::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--accent);
  transform-origin: left center;
  transform: scaleX(0);
  transition: transform 200ms var(--ease-out);
}
.link-arrow:hover {
  color: var(--accent);
}
.link-arrow:hover::after  { transform: scaleX(0); }
.link-arrow:hover::before {
  transform: scaleX(1);
  transition-delay: 180ms;
}
.link-arrow__arrow {
  display: inline-block;
  transition: transform var(--t-base) var(--ease-out);
}
.link-arrow:hover .link-arrow__arrow { transform: translateX(4px); }

/* disabled */
.link-arrow:disabled,
.link-arrow[aria-disabled="true"] {
  color: var(--fg-faint);
  cursor: not-allowed;
}
.link-arrow:disabled::after,
.link-arrow[aria-disabled="true"]::after { background: var(--line-soft); }
.link-arrow:disabled::before,
.link-arrow[aria-disabled="true"]::before { display: none; }
.link-arrow:disabled:hover { color: var(--fg-faint); }
.link-arrow:disabled:hover::after  { transform: none; }
.link-arrow:disabled:hover .link-arrow__arrow { transform: none; }

.link-arrow--s { font-size: var(--type-eyebrow); letter-spacing: var(--track-meta); text-transform: uppercase; padding-bottom: 3px; }
.link-arrow--m { font-size: 13px; letter-spacing: 0.05em; padding-bottom: 4px; }
.link-arrow--l { font-size: var(--type-h4); padding-bottom: 6px; }
.link-arrow--l:hover .link-arrow__arrow { transform: translateX(6px); }


/* ============================== 4 · image =========================
   <figure class="img"> wraps an <img> with lazy hydration + blur-up.
   The img starts blurred (CSS), JS swaps src when intersecting,
   then `.is-loaded` defocuses over 700ms.
   .img--warm adds a faint warm gradient overlay (NOT a darkening
   overlay — that's done by sections that need it).
*/
.img {
  position: relative;
  margin: 0;
  overflow: hidden;
  background: var(--bg-elev);
  isolation: isolate;
}
.img > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: blur(18px);
  transform: scale(1.04);
  transition:
    filter    var(--t-slow) var(--ease-out),
    transform var(--t-slower) var(--ease-out);
}
.img > img.is-loaded {
  filter: blur(0);
  transform: scale(1);
}
.img > figcaption {
  margin-top: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--type-caption);
  font-style: italic;
  color: var(--fg-muted);
  line-height: 1.5;
}

.img--warm::after {
  content: "";
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 60% 50% at 20% 80%,
      rgb(var(--img-tint-r) var(--img-tint-g) var(--img-tint-b) / 0.10),
      transparent 65%),
    linear-gradient(160deg, transparent 50%, rgb(0 0 0 / 0.25) 100%);
  pointer-events: none;
  mix-blend-mode: multiply;
}
.img--ratio-4-3 { aspect-ratio: 4 / 3; }
.img--ratio-3-4 { aspect-ratio: 3 / 4; }
.img--ratio-16-9 { aspect-ratio: 16 / 9; }
.img--ratio-21-9 { aspect-ratio: 21 / 9; }


/* ============================== 5 · card ==========================
   no border, no shadow, no fill. Layout is just an image and a body
   stacked; hover scales the image and the arrow appears.
*/
.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  color: inherit;
  text-decoration: none;
}
.card .img > img {
  transition:
    filter    var(--t-slower) var(--ease-out),
    transform var(--t-slower) var(--ease-out);
}
.card:hover .img > img.is-loaded { transform: scale(1.035); }
.card__body {
  display: flex; flex-direction: column;
  gap: var(--space-2);
}
.card__title {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: clamp(1.5rem, 2.2vw, 2rem);
  line-height: 1.12;
  letter-spacing: -0.015em;
  color: var(--fg);
  transition: color var(--t-base) var(--ease-out);
}
[lang^="zh"] .card__title { letter-spacing: 0; }
.card__desc {
  color: var(--fg-soft);
  font-size: var(--type-body);
  line-height: 1.6;
  max-width: 32ch;
}
.card__meta { font-size: var(--type-eyebrow); letter-spacing: var(--track-meta); text-transform: uppercase; color: var(--fg-muted); }
.card__arrow {
  display: inline-block;
  color: var(--accent);
  font-size: 15px;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity var(--t-base) var(--ease-out), transform var(--t-base) var(--ease-out);
}
.card:hover .card__arrow { opacity: 1; transform: translateY(0); }
.card:hover .card__title { color: var(--accent); }


/* ============================== 6 · spotlight =====================
   Pointer-tracked warm radial. Always-on (not hover-only) so the
   spotlight reads as ambient cinematic light rather than a hover
   easter-egg. mix-blend-mode: screen makes the gradient *add* light
   to whatever's beneath — on dark photography it warms; on cream
   reading sections it disappears (no muddy grey).

   We deliberately DON'T promote children with `.spotlight > *
   { z-index: 1 }` — that broke explicit negative z-indexes (hero__bg
   at z-index -3 etc). DOM order does the stacking; ::before paints
   in its natural place (just before the parent's first child), so
   normal-flow content sits on top automatically.
*/
.spotlight {
  position: relative;
}
.spotlight::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
    1200px circle at var(--mx, 50%) var(--my, 30%),
    oklch(0.78 0.12 65 / 0.22) 0%,
    oklch(0.65 0.08 70 / 0.10) 25%,
    transparent 55%
  );
  opacity: 0.55;
  mix-blend-mode: screen;
  transition: opacity var(--t-slow) var(--ease-out);
}
.spotlight:hover::before,
.spotlight:focus-within::before {
  opacity: 0.95;
}

/* --soft variant — smaller / dimmer for tighter writing-area glows
   (e.g. the hero text block). Otherwise identical mechanism. */
.spotlight--soft::before {
  background: radial-gradient(
    900px circle at var(--mx, 50%) var(--my, 40%),
    oklch(0.78 0.10 65 / 0.14) 0%,
    transparent 55%
  );
  opacity: 0.6;
}
.spotlight--soft:hover::before { opacity: 0.85; }

@media (prefers-reduced-motion: reduce) {
  .spotlight::before {
    transition: none;
    opacity: 0.6;
    --mx: 50%;
    --my: 30%;
  }
  .spotlight:hover::before { opacity: 0.6; }
}


/* ============================== 7 · text-reveal ===================
   spans are inserted by JS (.text-reveal__char each carrying a
   transition-delay). on `in-view`, opacity + translateY animate in.
   one-shot — observer disconnects after first hit.
*/
.text-reveal { display: inline; }
.text-reveal__char {
  display: inline-block;
  opacity: 0;
  transform: translateY(6px);
  transition:
    opacity   var(--t-slow) var(--ease-out),
    transform var(--t-slow) var(--ease-out);
  white-space: pre;             /* keep  -spaces if we used them */
}
.text-reveal.in-view .text-reveal__char {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .text-reveal__char {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}


/* ============================== 8 · form-input ====================
   underline-style input with a floating label. label sits inside the
   field, slides up + shrinks when input is focused or non-empty.
   :user-valid / :user-invalid drive the border colour after the
   user has interacted (avoids angry red on a fresh page).
   Tied to a <form> with `placeholder=" "` on each input.
*/
.field {
  position: relative;
  display: block;
  padding-top: 1.1rem;
}
.field + .field { margin-top: var(--space-5); }
.field__input {
  width: 100%;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  padding: 0.5rem 0 0.45rem;
  font-family: var(--font-sans);
  font-size: var(--type-h4);
  color: var(--fg);
  outline: 0;
  transition: border-color var(--t-base) var(--ease-out);
}
.field__input::placeholder { color: transparent; }
.field__label {
  position: absolute;
  left: 0;
  top: calc(1.1rem + 0.5rem);
  font-family: var(--font-sans);
  font-size: var(--type-h4);
  font-weight: 400;
  color: var(--fg-muted);
  pointer-events: none;
  transition: transform var(--t-base) var(--ease-out),
              color    var(--t-base) var(--ease-out),
              font-size var(--t-base) var(--ease-out),
              letter-spacing var(--t-base) var(--ease-out);
  transform-origin: left top;
}
.field__input:focus { border-color: var(--accent); }
.field__input:focus + .field__label,
.field__input:not(:placeholder-shown) + .field__label {
  transform: translateY(-1.4rem);
  font-size: var(--type-eyebrow);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--fg-faint);
}
.field__input:user-valid          { border-color: color-mix(in oklab, var(--accent) 60%, var(--line)); }
.field__input:user-invalid:not(:placeholder-shown) {
  border-color: oklch(0.58 0.16 25);   /* warm red, not signal red */
}
.field__input:user-invalid:not(:placeholder-shown) + .field__label { color: oklch(0.58 0.16 25); }

.field__hint  { display: block; margin-top: var(--space-2); font-size: var(--type-caption); color: var(--fg-muted); }
.field__error { display: block; margin-top: var(--space-2); font-size: var(--type-caption); color: oklch(0.58 0.16 25); }

/* affixed icon at end (e.g. eye for password) */
.field__suffix {
  position: absolute;
  right: 0;
  top: calc(1.1rem + 0.5rem);
  background: none; border: 0; padding: 0;
  color: var(--fg-muted);
  cursor: pointer;
  transition: color var(--t-base) var(--ease-out);
}
.field__suffix:hover { color: var(--fg); }
.field__suffix .ic { width: 18px; height: 18px; }


/* ============================== 9 · dialog ========================
   native <dialog class="modal">, fade + scale via JS class toggle
   (the browsers that have @starting-style stable will use it through
   the same `[open]` selector; older Safari falls back to RAF class
   we set in JS).
*/
.modal {
  border: 0;
  padding: 0;
  margin: auto;
  max-width: 32rem;
  width: 90%;
  background: var(--bg-elev);
  color: var(--fg);
  font-family: var(--font-sans);
  opacity: 0;
  transform: scale(0.965);
  transition:
    opacity   var(--t-base) var(--ease-out),
    transform var(--t-base) var(--ease-out),
    overlay   var(--t-base) var(--ease-out) allow-discrete,
    display   var(--t-base) var(--ease-out) allow-discrete;
}
.modal[open] { opacity: 1; transform: scale(1); }
@starting-style {
  .modal[open] { opacity: 0; transform: scale(0.965); }
}
.modal::backdrop {
  background: rgb(0 0 0 / 0);
  backdrop-filter: blur(0);
  transition: background var(--t-base) var(--ease-out),
              backdrop-filter var(--t-base) var(--ease-out);
}
.modal[open]::backdrop {
  background: rgb(0 0 0 / 0.55);
  backdrop-filter: blur(6px);
}
@starting-style {
  .modal[open]::backdrop {
    background: rgb(0 0 0 / 0);
    backdrop-filter: blur(0);
  }
}

.modal__inner    { padding: clamp(1.5rem, 4vw, 2.5rem); position: relative; }
.modal__close    {
  position: absolute;
  top: 12px; right: 12px;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg-muted);
  transition: color var(--t-base) var(--ease-out);
}
.modal__close:hover { color: var(--fg); }
.modal__close .ic   { width: 16px; height: 16px; }

.modal__eyebrow { display: block; margin-bottom: var(--space-3); }
.modal__title   {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: var(--type-h3);
  line-height: 1.15;
  margin: 0 0 var(--space-3);
  color: var(--fg);
}
[lang^="zh"] .modal__title { letter-spacing: 0; }
.modal__desc { color: var(--fg-soft); font-size: var(--type-body); line-height: 1.6; }


/* ============================== 9b · film grain ===================
   Site-wide SVG turbulence noise pinned over the viewport. Opacity is
   low enough that you only notice it on solid colour fields (kills
   banding); blend-mode: overlay keeps it neutral on both dark and
   light themes. Pointer-events off so it never eats clicks. */
.grain {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 9997;
  pointer-events: none;
  opacity: 0.06;
  mix-blend-mode: overlay;
}
@media (prefers-reduced-motion: reduce) {
  .grain { opacity: 0.04; }
}


/* ============================== 10 · scroll-progress ==============
   1px line pinned to the top of the viewport. Width tracks how far
   the page has scrolled. Accent at 60 % opacity — never punchy.
*/
.scroll-progress {
  position: fixed;
  top: 0; left: 0;
  height: 1px;
  width: 0;
  background: var(--accent);
  opacity: 0.6;
  z-index: 100;
  pointer-events: none;
  transition: width 80ms linear;
}


/* ============================== 11 · stagger reveal ===============
   Containers tagged [data-stagger] reveal their direct children with
   80 ms steps once the container enters the viewport. The brief
   pegged "80 ms not 200 ms" — anything slower reads as SaaS.
*/
[data-stagger] > * {
  opacity: 0;
  transform: translateY(16px);
  transition:
    opacity   700ms var(--ease-out),
    transform 700ms var(--ease-out);
}
[data-stagger].in-view > *           { opacity: 1; transform: none; }
[data-stagger].in-view > *:nth-child(1) { transition-delay:   0ms; }
[data-stagger].in-view > *:nth-child(2) { transition-delay:  80ms; }
[data-stagger].in-view > *:nth-child(3) { transition-delay: 160ms; }
[data-stagger].in-view > *:nth-child(4) { transition-delay: 240ms; }
[data-stagger].in-view > *:nth-child(5) { transition-delay: 320ms; }
[data-stagger].in-view > *:nth-child(6) { transition-delay: 400ms; }
@media (prefers-reduced-motion: reduce) {
  [data-stagger] > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}


/* ============================== icons =============================
   Phosphor-light–style inline SVG (256x256 viewBox, stroke 12, no
   fill, round joins). Pages drop the <svg class="ic"> directly.
*/
.ic {
  display: inline-block;
  width: 1em; height: 1em;
  flex-shrink: 0;
  vertical-align: middle;
  fill: none;
  stroke: currentColor;
  stroke-width: 12;
  stroke-linecap: round;
  stroke-linejoin: round;
}
