/* ============================================================
   SHELL MOTION · 观测站交互动效与边距仪表
   Shared by every public route inside PublicObservatoryShell.

   - Motion vocabulary: receive (进入视野), lock (按下/聚焦), retune
     (主题、档案切换), unfold (展开). Each state change has one
     direction and one duration family; nothing loops by itself.
   - Every animation lives inside prefers-reduced-motion: no-preference.
     Reduced motion keeps the final state and loses only the movement.
   - Content is never hidden by a pending animation: entrances are
     scroll-driven where supported, otherwise the page renders as-is.
   - Margin instruments are aria-hidden decoration, desktop-only and
     drawn behind content (z-index 0, pointer-events none).
   ============================================================ */

:root {
  --motion-fast: .16s;
  --motion-base: .28s;
  --motion-slow: .52s;
  --motion-press: cubic-bezier(.3, .7, .4, 1);
  --motion-settle: cubic-bezier(.16, 1, .3, 1);
  --shell-rail-ink: var(--line-strong);
  --shell-rail-faint: var(--line);
  --shell-rail-lit: var(--signal-text);
}

/* ---------- Margin instruments (built by platform/shell-motion.js) ---------- */
.shell-rails {
  position: fixed;
  inset: var(--public-shell-header-h, var(--header-h, 56px)) 0 0;
  z-index: 0;
  display: none;
  pointer-events: none;
}

@media (min-width: 1360px) and (min-height: 620px) {
  .shell-rails { display: block; }
}

.shell-rail {
  position: absolute;
  top: 16vh;
  bottom: 18vh;
  width: 18px;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
}

/* Left: section map. The spine is the whole page; ticks are sections;
   the bracket is the part of the page currently in the viewport. */
.shell-rail-map { left: 26px; }

.shell-rail-map::before {
  position: absolute;
  inset: 0 auto 0 0;
  width: 1px;
  background: var(--shell-rail-faint);
  content: "";
}

.shell-rail-map i {
  position: absolute;
  top: var(--at, 0%);
  left: 0;
  width: 6px;
  height: 1px;
  background: var(--shell-rail-ink);
  transform-origin: left;
  transition: width var(--motion-slow) var(--motion-settle), background-color var(--motion-base);
}

.shell-rail-map i.is-current {
  width: 14px;
  background: var(--shell-rail-lit);
}

.shell-rail-window {
  position: absolute;
  top: 0;
  left: -2px;
  width: 5px;
  height: calc(var(--view-size, .1) * 100%);
  min-height: 12px;
  border-block: 1px solid color-mix(in srgb, var(--shell-rail-lit) 70%, transparent);
  background: linear-gradient(to right, transparent 2px, color-mix(in srgb, var(--shell-rail-lit) 70%, transparent) 2px 3px, transparent 3px);
  transform: translateY(calc(var(--view-top, 0) * var(--rail-h, 0px)));
}

/* Right: a measuring scale that travels past a fixed index needle. */
.shell-rail-scale { right: 26px; }

.shell-rail-scale::before {
  position: absolute;
  inset: 0 0 0 auto;
  width: 9px;
  background-image:
    linear-gradient(to bottom, var(--shell-rail-ink) 1px, transparent 1px),
    linear-gradient(to bottom, var(--shell-rail-ink) 1px, transparent 1px);
  background-position:
    right 0 top calc(var(--scale-shift, 0) * 1px),
    right 0 top calc(var(--scale-shift, 0) * 1px);
  background-size: 9px 60px, 4px 12px;
  background-repeat: repeat-y;
  opacity: .9;
  content: "";
}

.shell-rail-scale::after {
  position: absolute;
  inset: 0 0 0 auto;
  width: 1px;
  background: var(--shell-rail-faint);
  content: "";
}

.shell-rail-needle {
  position: absolute;
  top: 50%;
  right: 0;
  width: 15px;
  height: 1px;
  background: linear-gradient(to right, var(--shell-rail-lit) 3px, transparent 3px 5px, var(--shell-rail-lit) 5px);
}

html[data-theme="light"] .shell-rails { --shell-rail-ink: rgba(29, 35, 42, .26); }

body.developer-mode .shell-rails,
body.lab-route:has(.lab-experiment) .shell-rails { display: none; }

/* ---------- Article rail locator (aria-current set by shell-motion.js) ---------- */
.article-page-rail a {
  position: relative;
  transition: color var(--motion-base);
}

.article-page-rail a::before {
  position: absolute;
  top: .78em;
  left: -15px;
  width: 1px;
  height: 0;
  background: var(--signal-text);
  content: "";
  transform: translateY(-50%);
  transition: height var(--motion-slow) var(--motion-settle);
}

.article-page-rail a[aria-current="location"] { color: var(--ink); }
.article-page-rail a[aria-current="location"] span { color: var(--signal-text); }
.article-page-rail a[aria-current="location"]::before { height: 1.55em; }

/* ---------- Media reticle: corner marks close in on the pointer ---------- */
:is(.post-image, .friend-media, .lab-entry-preview)::after {
  --reticle: color-mix(in srgb, var(--signal-text) 80%, transparent);
  position: absolute;
  inset: 10px;
  z-index: 2;
  background:
    linear-gradient(var(--reticle), var(--reticle)) left top / 14px 1px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) left top / 1px 14px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) right top / 14px 1px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) right top / 1px 14px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) left bottom / 14px 1px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) left bottom / 1px 14px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) right bottom / 14px 1px no-repeat,
    linear-gradient(var(--reticle), var(--reticle)) right bottom / 1px 14px no-repeat;
  content: "";
  opacity: 0;
  pointer-events: none;
}

.friend-media,
.lab-entry-preview { position: relative; }

.friend-item.uses-site-icon .friend-media::after { content: none; }

:is(.post-card:hover .post-image, .post-card:focus-within .post-image, .friend-item:hover .friend-media, .friend-item:focus-within .friend-media, .lab-entry:hover .lab-entry-preview, .lab-entry.is-preview .lab-entry-preview)::after {
  opacity: 1;
}

/* ---------- Heading registration mark ---------- */
.public-observatory-shell .section-heading h2,
.public-observatory-shell .changelog-hero h1 {
  position: relative;
}

.public-observatory-shell .section-heading h2::before,
.public-observatory-shell .changelog-hero h1::before {
  position: absolute;
  top: -16px;
  left: 0;
  width: 28px;
  height: 1px;
  background: linear-gradient(to right, var(--signal-text) 3px, transparent 3px 6px, color-mix(in srgb, var(--signal-text) 60%, transparent) 6px);
  content: "";
  transform-origin: left;
}

@media (prefers-reduced-motion: no-preference) {
  /* ---------- Press: controls answer with a brief, physical lock ---------- */
  .icon-button,
  .filter-chip,
  .project-terminal-navigation button,
  .menu-button,
  .lab-start,
  .music-easter-toggle,
  .article-close,
  .text-link,
  .public-shell-footer #footer-build-number {
    transition-property: color, border-color, background-color, box-shadow, opacity, scale;
    transition-duration: .3s, .3s, .3s, .3s, .3s, var(--motion-fast);
    transition-timing-function: ease, ease, ease, ease, ease, var(--motion-press);
  }

  :is(.icon-button, .filter-chip, .project-terminal-navigation button, .menu-button, .lab-start, .music-easter-toggle, .article-close, .text-link, .public-shell-footer #footer-build-number):active:not(:disabled) {
    scale: .95;
    transition-duration: .3s, .3s, .3s, .3s, .3s, .06s;
  }

  .icon-button .icon { transition: rotate var(--motion-slow) var(--motion-settle); }
  #searchToggle:hover .icon { rotate: -12deg; }
  .back-top { transition: color .3s, border-color .3s, scale var(--motion-fast) var(--motion-press); }
  .back-top:active { scale: .95; }
  .back-top .icon { transition: translate var(--motion-base) var(--motion-settle); }
  .back-top:hover .icon { translate: 0 -2px; }

  /* Icon swap (theme / audio) after first paint only. */
  html.shell-motion-ready :is(#themeToggle, #archiveThemeToggle, #platformThemeToggle) .icon,
  html.shell-motion-ready #audioToggle .icon {
    animation: shell-icon-retune var(--motion-slow) var(--motion-settle);
  }

  /* Filter chips: the diamond turns a quarter when its channel locks. */
  html.shell-motion-ready .filter-chip.is-active::before {
    animation: shell-chip-lock .46s var(--motion-settle);
  }

  /* Focus: the ring closes in from a wider radius, like a reticle locking. */
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    animation: shell-focus-lock var(--motion-base) var(--motion-settle);
  }

  /* Link arrows travel along their own direction. */
  .read-link,
  .lab-entry-caption a span,
  .home-project-links a span,
  .friend-copy i {
    transition: color .3s, translate var(--motion-base) var(--motion-settle), transform .3s var(--motion-settle);
  }
  .post-card:hover .read-link { translate: 3px 0; }
  .lab-entry:hover .lab-entry-caption a span,
  .lab-entry-caption a:focus-visible span { translate: 2px -2px; }
  .home-project-links a:hover span { translate: 2px -1px; }

  /* Media reticle closes from a wider frame. */
  :is(.post-image, .friend-media, .lab-entry-preview)::after {
    scale: 1.05;
    transition: opacity var(--motion-base), scale var(--motion-slow) var(--motion-settle);
  }
  :is(.post-card:hover .post-image, .post-card:focus-within .post-image, .friend-item:hover .friend-media, .friend-item:focus-within .friend-media, .lab-entry:hover .lab-entry-preview, .lab-entry.is-preview .lab-entry-preview)::after {
    scale: 1;
  }

  /* ---------- Dialogs: receive from below, return the same way ---------- */
  :is(.search-dialog, .secret-dialog, .signal-tuner-dialog, .image-inspector-dialog, .room-record-dialog, .protocol-dialog, .article-dialog) {
    opacity: 0;
    translate: 0 14px;
    scale: .985;
    transition:
      opacity var(--motion-base) ease,
      translate var(--motion-slow) var(--motion-settle),
      scale var(--motion-slow) var(--motion-settle),
      overlay var(--motion-base) allow-discrete,
      display var(--motion-base) allow-discrete;
  }

  :is(.search-dialog, .secret-dialog, .signal-tuner-dialog, .image-inspector-dialog, .room-record-dialog, .protocol-dialog, .article-dialog)[open] {
    opacity: 1;
    translate: 0 0;
    scale: 1;
  }

  .article-dialog { scale: 1; translate: 0 24px; }

  @starting-style {
    :is(.search-dialog, .secret-dialog, .signal-tuner-dialog, .image-inspector-dialog, .room-record-dialog, .protocol-dialog, .article-dialog)[open] {
      opacity: 0;
      translate: 0 14px;
      scale: .985;
    }
    .article-dialog[open] { translate: 0 24px; scale: 1; }
  }

  :is(.search-dialog, .secret-dialog, .signal-tuner-dialog, .image-inspector-dialog, .room-record-dialog, .protocol-dialog, .article-dialog)::backdrop {
    opacity: 0;
    transition: opacity var(--motion-slow) ease, overlay var(--motion-slow) allow-discrete, display var(--motion-slow) allow-discrete;
  }

  :is(.search-dialog, .secret-dialog, .signal-tuner-dialog, .image-inspector-dialog, .room-record-dialog, .protocol-dialog, .article-dialog)[open]::backdrop { opacity: 1; }

  @starting-style {
    :is(.search-dialog, .secret-dialog, .signal-tuner-dialog, .image-inspector-dialog, .room-record-dialog, .protocol-dialog, .article-dialog)[open]::backdrop { opacity: 0; }
  }

  /* Search results arrive in reading order. */
  .search-results > * { animation: shell-receive-soft .42s var(--motion-settle) both; }
  .search-results > :nth-child(2) { animation-delay: 30ms; }
  .search-results > :nth-child(3) { animation-delay: 60ms; }
  .search-results > :nth-child(4) { animation-delay: 90ms; }
  .search-results > :nth-child(5) { animation-delay: 120ms; }
  .search-results > :nth-child(n+6) { animation-delay: 150ms; }

  /* Image inspector magnification eases instead of jumping. */
  .image-inspector-shell .image-inspector-stage img { transition: width var(--motion-base) var(--motion-settle); }

  /* ---------- Unfold: details open and close with their real height ---------- */
  :is(.free-details, .home-project-details, .lab-instructions, .resilience-details) {
    interpolate-size: allow-keywords;
  }

  :is(.free-details, .home-project-details, .lab-instructions, .resilience-details)::details-content {
    block-size: 0;
    overflow: clip;
    opacity: 0;
    transition:
      block-size var(--motion-slow) var(--motion-settle),
      opacity var(--motion-base) ease,
      content-visibility var(--motion-slow) allow-discrete;
  }

  :is(.free-details, .home-project-details, .lab-instructions, .resilience-details)[open]::details-content {
    block-size: auto;
    opacity: 1;
  }

  .free-details summary::after { transition: transform var(--motion-slow) var(--motion-settle); }

  /* ---------- Retune: about portrait scans to the next frame ---------- */
  .about-portrait img {
    transition: opacity .19s ease, clip-path .5s var(--motion-settle), scale .6s var(--motion-settle);
    clip-path: inset(0);
  }
  .about-portrait.is-switching img {
    clip-path: inset(0 0 100% 0);
    scale: 1.02;
    transition: opacity .19s ease, clip-path .19s ease-in, scale .19s ease-in;
  }

  /* Project dossiers slide in the direction of travel (set by home-models.js). */
  .project-terminal-panels[data-motion] > .home-project-dossier:not([hidden]) :is(.home-project-header, .home-project-frame) {
    animation: shell-dossier-in .56s var(--motion-settle) both;
  }
  .project-terminal-panels[data-motion] > .home-project-dossier:not([hidden]) .home-project-frame { animation-delay: 60ms; }
  .project-terminal-panels[data-motion="prev"] { --dossier-from: -18px; }

  /* Route directory on phones: same receive as the home menu, and it fades
     back out instead of vanishing (display is kept until the fade ends). */
  .platform-mobile-nav {
    transition: opacity var(--motion-base) ease, display var(--motion-base) allow-discrete, overlay var(--motion-base) allow-discrete;
  }
  .platform-mobile-nav[hidden] { opacity: 0; pointer-events: none; }
  @starting-style { .platform-mobile-nav:not([hidden]) { opacity: 0; } }
  .platform-mobile-nav:not([hidden]) > header { animation: shell-receive-soft .42s var(--motion-settle) both; }
  .platform-mobile-nav:not([hidden]) nav a { animation: shell-receive .5s var(--motion-settle) both; }
  .platform-mobile-nav:not([hidden]) nav a:nth-child(2) { animation-delay: 50ms; }
  .platform-mobile-nav:not([hidden]) nav a:nth-child(3) { animation-delay: 100ms; }
  .platform-mobile-nav:not([hidden]) nav a:nth-child(4) { animation-delay: 150ms; }
  .platform-mobile-nav:not([hidden]) nav a:nth-child(n+5) { animation-delay: 200ms; }

  /* Comment reply target slides in under the form fields. */
  .article-echo-target {
    transition: opacity var(--motion-base) ease, translate var(--motion-slow) var(--motion-settle), display var(--motion-base) allow-discrete;
  }
  .article-echo-target[hidden] { opacity: 0; translate: 0 -6px; }
  @starting-style { .article-echo-target:not([hidden]) { opacity: 0; translate: 0 -6px; } }

  /* Filtered lists: re-rendered cards arrive in order (flag set by shell-motion.js). */
  [data-motion-refresh] > * { animation: shell-receive-soft .48s var(--motion-settle) both !important; }
  [data-motion-refresh] > :nth-child(2) { animation-delay: 45ms !important; }
  [data-motion-refresh] > :nth-child(3) { animation-delay: 90ms !important; }
  [data-motion-refresh] > :nth-child(4) { animation-delay: 135ms !important; }
  [data-motion-refresh] > :nth-child(n+5) { animation-delay: 180ms !important; }

  /* ---------- Theme retune: the new theme opens from the toggle ---------- */
  html.is-theme-retuning::view-transition-old(root),
  html.is-theme-retuning::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
  }

  html.is-theme-retuning::view-transition-new(root) {
    animation: shell-theme-wipe .66s var(--ease-swift) both;
  }

  /* Margin scale travels smoothly between frames. */
  .shell-rail-window { transition: height var(--motion-base) ease; }
}

/* ---------- Receive: one-time arrival just before an element scrolls in ----------
   shell-motion.js adds .is-receiving only to elements still below the fold, so a
   static page (no JavaScript, print, failed script) is never hidden. */
@media (prefers-reduced-motion: no-preference) {
  .is-receiving {
    animation: shell-receive .7s var(--motion-settle) both;
    animation-delay: var(--receive-delay, 0ms);
  }

  .is-receiving h2::before,
  h2.is-receiving::before {
    animation: shell-mark-draw .9s var(--motion-settle) both;
    animation-delay: calc(var(--receive-delay, 0ms) + 180ms);
  }
}

@keyframes shell-receive {
  from { opacity: 0; translate: 0 22px; }
}

@keyframes shell-receive-soft {
  from { opacity: 0; translate: 0 10px; }
}

@keyframes shell-mark-draw {
  from { scale: 0 1; }
}

@keyframes shell-icon-retune {
  from { opacity: 0; rotate: -70deg; scale: .6; }
}

@keyframes shell-chip-lock {
  from { rotate: -90deg; scale: .4; }
}

@keyframes shell-focus-lock {
  from { outline-offset: 7px; outline-color: transparent; }
}

@keyframes shell-dossier-in {
  from { opacity: 0; translate: var(--dossier-from, 18px) 0; }
}

@keyframes shell-theme-wipe {
  from { clip-path: circle(0 at var(--theme-wipe-x, 100%) var(--theme-wipe-y, 0)); }
  to { clip-path: circle(var(--theme-wipe-r, 150vmax) at var(--theme-wipe-x, 100%) var(--theme-wipe-y, 0)); }
}

@media print {
  .shell-rails,
  :is(.post-image, .friend-media, .lab-entry-preview)::after,
  .public-observatory-shell .section-heading h2::before,
  .public-observatory-shell .changelog-hero h1::before { display: none !important; }
}
