/* Loaded after every component stylesheet and before wuji.css (see the link
   order in views/partials/head.ejs). The reveal, the preference switches,
   print and the view transitions override component rules at equal
   specificity, so they have to come later in the cascade. */

/* ==========================================================================
   Reveal on scroll
   --------------------------------------------------------------------------
   site.js marks .is-visible (at once under reduced motion). Scoped to
   .has-js, which theme.js sets before first paint, so a blocked script never
   leaves content at opacity 0.
   An animation on the individual translate property, with a backwards fill
   only, so the element's own transition, transform and opacity rules (hover,
   press, disabled) are untouched once it has arrived.
   Views write --i (0, 1, 2, ...) on siblings that arrive together; each
   step waits 40ms, capped at six steps.
   .reveal belongs on a wrapper, never on a .glass surface: an element part
   way through an opacity animation is a backdrop root, so a blur inside it
   samples nothing until the fade ends. A .reveal.glass rises only.
   ========================================================================== */

.has-js .reveal:not(.is-visible) {
  opacity: 0;
  translate: 0 14px;
}
.has-js .reveal.is-visible {
  animation: reveal-in var(--dur-reveal) var(--ease-entrance) calc(min(var(--i, 0), 6) * 40ms) backwards;
}
.has-js .reveal.glass:not(.is-visible) { opacity: 1; }
.has-js .reveal.glass.is-visible { animation-name: reveal-rise; }
@keyframes reveal-in {
  from { opacity: 0; translate: 0 14px; }
}
@keyframes reveal-rise {
  from { translate: 0 14px; }
}

/* A heading's text (wrapped in span.rise by the view) rises 0.4em inside
   its own clip, in step with its .reveal. clip-path rather than overflow so
   descenders survive (the -0.2em bottom inset). */
.has-js .reveal :is(h1, h2, h3):has(> .rise) { clip-path: inset(0 -0.5em -0.2em); }
.has-js .reveal :is(h1, h2, h3) > .rise { display: block; }
.has-js .reveal:not(.is-visible) :is(h1, h2, h3) > .rise { translate: 0 0.4em; }
.has-js .reveal.is-visible :is(h1, h2, h3) > .rise {
  animation: rise-in var(--dur-reveal) var(--ease-entrance) calc(min(var(--i, 0), 6) * 40ms) backwards;
}
@keyframes rise-in {
  from { translate: 0 0.4em; }
}

/* ==========================================================================
   Reduced motion
   --------------------------------------------------------------------------
   The backstop. Durations collapse to 0.001ms rather than none, so every
   transitionend and animationend that script waits for still fires; delays
   go to 0 so nothing sits on an invisible start frame first. Each component
   declares its own loops inside prefers-reduced-motion: no-preference, so
   under this setting they are never started (the ambient field shows the
   app's still frame for the same reason).
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  .has-js .reveal:not(.is-visible) { opacity: 1; translate: none; }
  .has-js .reveal:not(.is-visible) :is(h1, h2, h3) > .rise { translate: none; }
  /* Scroll-driven, not autonomous: it only moves as far as the reader
     scrolls, so it keeps its scroll timeline (auto is the timeline's own
     duration). */
  .read-progress { animation-duration: auto !important; }
}

/* ==========================================================================
   Preference and capability switches
   --------------------------------------------------------------------------
   Every glass surface reads its tint, tones, washes, rims and blurs from
   tokens.css, so each of these is a token switch in one place. The light
   selectors weigh the same as the light rules in tokens.css and win here by
   order. The blurs go to none rather than blur(0) so an opaque surface is
   not left a backdrop root and a composited layer.
   ========================================================================== */

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root { --glass-on: 0; --glass-tint: #1b1c24; }
  :root[data-theme='light'] { --glass-tint: #fbfbfe; }
}

@media (prefers-reduced-transparency: reduce) {
  :root {
    --glass-on: 0;
    --glass-tint: #1b1c24;
    --tone-chrome: rgb(0 0 0 / .2);
    --tone-pane: rgb(0 0 0 / .2);
    --tone-sheet: rgb(0 0 0 / .2);
    --tone-dock: rgb(0 0 0 / .2);
    --blur-bubble: none; --blur-fill: none; --blur-list: none; --blur-card: none; --blur-dock: none;
    --blur-chrome: none; --blur-pane: none; --blur-sheet: none; --blur-scrim: none;
  }
  :root[data-theme='light'] {
    --glass-tint: #fbfbfe;
    --tone-chrome: rgb(255 255 255 / 0);
    --tone-pane: rgb(255 255 255 / 0);
    --tone-sheet: rgb(255 255 255 / 0);
    --tone-dock: rgb(255 255 255 / 0);
  }
}

/* More contrast: surfaces solid, washes gone except the selection ones (34,
   40, 62 and --mat-select, so a selected state stays visible), rims become a
   ring, hairlines and etched lines darken, secondary text moves up to
   --text-soft. */
@media (prefers-contrast: more) {
  :root, :root[data-theme='light'] {
    --glass-on: 0;
    --glass-tint: var(--surface-solid);
    --ink-2: var(--text-soft);
    --hairline: rgb(var(--ink) / .28);
    --etch-line: rgb(var(--ink) / .42);
    --blur-bubble: none; --blur-fill: none; --blur-list: none; --blur-card: none; --blur-dock: none;
    --blur-chrome: none; --blur-pane: none; --blur-sheet: none; --blur-scrim: none;
  }
  :root, [data-accent], :root[data-theme='light'], :root[data-theme='light'] [data-accent] {
    --wash-10: linear-gradient(transparent, transparent);
    --wash-12: linear-gradient(transparent, transparent);
    --wash-16: linear-gradient(transparent, transparent);
    --wash-26: linear-gradient(transparent, transparent);
    --wash-30: linear-gradient(rgb(var(--ink) / .12), rgb(var(--ink) / .12));
    --glass-rim: inset 0 0 0 1px color-mix(in srgb, var(--text) 55%, transparent);
  }
}

/* Weak devices (theme.js sets data-glass='lite' before paint): only floating
   chrome keeps a real blur, and the ambient field holds its still frame. */
html[data-glass='lite'] {
  --blur-fill: none;
  --blur-list: none;
  --blur-card: none;
}
html[data-glass='lite'] :is(.amb, .amb > i) { animation: none; }

@media (forced-colors: active) {
  :is(.header-inner, .tab-bar-body, .popover, .search-results, .cookie-notice-panel, .group, .card, .doc-card, .post-card, .source-card, .panel,
      .tag, .btn, .period-tabs, .docs-sidebar-inner, .crop-modal, .table-scroll, .callout, .banner, .note, .flash) { border: 1px solid CanvasText; }
  :is(.site-nav a[aria-current='page'], .tag.is-active, .period-tab.is-active, .period-tab[aria-current='page'], .docs-sidebar a[aria-current='page'],
      .tab-bar-item[aria-current='page'], .swatch[aria-pressed='true']) { forced-color-adjust: none; background: Highlight; color: HighlightText; outline: 2px solid CanvasText; }
  .tab-bar-bubble, .ambient, .hero-bloom { display: none; }
}

/* ==========================================================================
   Print
   --------------------------------------------------------------------------
   Paper is white whatever the theme, and nearly every rule reads a theme
   token rather than inheriting body's colour, so print takes the light
   theme's measured values wholesale (tokens.css section 3). Glass goes flat,
   accent surfaces lose their fill (background graphics are usually off, and
   a white label would print white on white), and chrome is screen only.
   ========================================================================== */

@media print {
  :root {
    color-scheme: light;
    --bg: #f6f7fc;
    --bg-soft: #ffffff;
    --surface: rgba(255, 255, 255, 0.75);
    --surface-solid: #ffffff;
    --surface-raised: #ffffff;
    --border: rgba(20, 22, 40, 0.1);
    --border-strong: rgba(20, 22, 40, 0.18);
    --text: #14161f;
    --text-soft: #414659;
    --text-muted: #666d84;
    --ink-2: color-mix(in srgb, var(--text) 16%, var(--text-muted));
    --ink-2-strong: var(--text-soft);
    --ink: 0 0 0;
    --hairline: rgb(var(--ink) / .10);
    --teal-ink-share: 36%;
    --violet-ink-share: 85%;
    --link: color-mix(in srgb, var(--violet) 65%, black);
    --mark: color-mix(in srgb, var(--violet) 16%, transparent);
    --code-bg: #f2f3fa;
    --field-error-ink: #c32e46;
    --status-safe-ink: #1f7a45;
    --status-caution-ink: #8a5a12;
    --glass-on: 0;
    --glass-tint: #ffffff;
    --tone-chrome: transparent; --tone-dock: transparent; --tone-pane: transparent; --tone-sheet: transparent;
    --etch-fill: rgb(var(--ink) / .05);
    --etch-line: rgb(var(--ink) / .12);
  }
  :root, [data-accent] {
    --tint-ink-share: 62%;
    --glass-rim: inset 0 0 0 1px rgb(0 0 0 / .12);
    --mat-select: var(--mat-etched);
    --mat-accent: var(--mat-etched);
    --mat-accent-hover: var(--mat-etched);
    --mat-bubble: var(--mat-etched);
    --on-select: var(--text);
    --on-select-shadow: none;
    --on-accent: var(--text);
    --on-accent-shadow: none;
    --wordmark-gradient: linear-gradient(var(--text), var(--text));
  }
  html, body { background: #fff; color: #000; }
  .site-header, .site-footer, .tab-bar, .search-results, .appearance-panel, .cookie-notice,
  .docs-sidebar, .docs-toc, .docs-local-nav, .doc-pager, .post-nav, .hero-atmosphere { display: none; }
  /* A page printed before it was scrolled would otherwise print every
     unrevealed section blank. */
  .has-js .reveal:not(.is-visible) { opacity: 1; translate: none; }
  .has-js .reveal:not(.is-visible) :is(h1, h2, h3) > .rise { translate: none; }
  .prose { max-width: none; }
  .docs-layout, .post-layout {
    display: block;
    width: auto;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
  }
}

/* ==========================================================================
   Cross-document view transitions
   --------------------------------------------------------------------------
   Every link is a real navigation; @view-transition makes it read as one
   app. Browsers without it ignore the rule and navigate instantly.
   ========================================================================== */

@view-transition {
  navigation: auto;
}

/* A 300ms fade-through: the old page fades out over 240ms, the new one fades
   in from 60ms to 300ms. The four navigation types site.js sets (see its
   own comment for how it picks one) add a 12px shift in their direction;
   an untyped navigation is the plain fade-through. */
::view-transition-old(root) { animation: vt-fade-out 240ms var(--ease-accel) both; }
::view-transition-new(root) { animation: vt-fade-in 240ms var(--ease-decel) 60ms both; }
@keyframes vt-fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}
@keyframes vt-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* forward and back: along a row of peers (one guide to the next, the back
   button). deeper and shallower: in and out (the docs index into a guide, a
   post back out to the blog). The arriving page always comes from the side
   the leaving page moves away from. */
html:active-view-transition-type(forward)::view-transition-old(root) { animation-name: vt-out-left; }
html:active-view-transition-type(forward)::view-transition-new(root) { animation-name: vt-in-right; }
html:active-view-transition-type(back)::view-transition-old(root) { animation-name: vt-out-right; }
html:active-view-transition-type(back)::view-transition-new(root) { animation-name: vt-in-left; }
html:active-view-transition-type(deeper)::view-transition-old(root) { animation-name: vt-out-up; }
html:active-view-transition-type(deeper)::view-transition-new(root) { animation-name: vt-in-below; }
html:active-view-transition-type(shallower)::view-transition-old(root) { animation-name: vt-out-down; }
html:active-view-transition-type(shallower)::view-transition-new(root) { animation-name: vt-in-above; }
@keyframes vt-out-left { to { opacity: 0; transform: translateX(-12px); } }
@keyframes vt-out-right { to { opacity: 0; transform: translateX(12px); } }
@keyframes vt-out-up { to { opacity: 0; transform: translateY(-12px); } }
@keyframes vt-out-down { to { opacity: 0; transform: translateY(12px); } }
@keyframes vt-in-right { from { opacity: 0; transform: translateX(12px); } }
@keyframes vt-in-left { from { opacity: 0; transform: translateX(-12px); } }
@keyframes vt-in-below { from { opacity: 0; transform: translateY(12px); } }
@keyframes vt-in-above { from { opacity: 0; transform: translateY(-12px); } }

/* The header capsule is identical on every page, so it holds still: the new
   image replaces the old outright. Both images are translucent glass, so
   showing the old one under the new would double the tint for the length of
   the transition. Named on .header-inner, never also on .site-header: two
   elements with one name abort the whole transition. */
.site-header .header-inner { view-transition-name: site-header; }
::view-transition-old(site-header) { animation: none; opacity: 0; }
::view-transition-new(site-header) { animation: none; }

/* The docs sidebar holds still between two docs the same way. Where it
   exists on one side only (:only-child), it fades with the page. The outline
   changes with every doc and is unboxed text, so it cross-fades in place
   instead of sliding with the article. */
.docs-sidebar { view-transition-name: docs-sidebar; }
.docs-toc { view-transition-name: docs-toc; }
::view-transition-old(docs-sidebar) { animation: none; opacity: 0; }
::view-transition-new(docs-sidebar) { animation: none; }
::view-transition-old(docs-sidebar):only-child { animation: vt-fade-out 240ms var(--ease-accel) both; }
::view-transition-new(docs-sidebar):only-child { animation: vt-fade-in 240ms var(--ease-decel) 60ms both; }
::view-transition-old(docs-toc) { animation: vt-fade-out 240ms var(--ease-accel) both; }
::view-transition-new(docs-toc) { animation: vt-fade-in 240ms var(--ease-decel) 60ms both; }

/* Shared marks: a doc's icon mark (doc-banner-<slug>) and a post's
   (post-<slug>) travel from the list into the page header. The views write
   those names inline, so the class is matched on the attribute and keeps
   working whichever element carries it. */
[style*='view-transition-name: doc-banner-'],
[style*='view-transition-name:doc-banner-'] { view-transition-class: doc-banner; }
[style*='view-transition-name: post-'],
[style*='view-transition-name:post-'] { view-transition-class: post-badge; }
::view-transition-group(.doc-banner),
::view-transition-group(.post-badge) {
  animation-duration: 380ms;
  animation-timing-function: var(--ease-out);
}
/* Both images stretch with the group as it resizes; left at their own
   aspect ratios they would expose a corner mid-flight. */
::view-transition-old(.doc-banner),
::view-transition-new(.doc-banner) {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The theme and palette switch (site.js runs it as a same-document
   transition typed "theme"): nothing moves, so every snapshot, the held ones
   included, cross-fades. plus-lighter keeps a cross-fade between two opaque
   images from dipping in the middle. Explicit from frames, because the held
   snapshots above rest at opacity 0. */
html:active-view-transition-type(theme)::view-transition-old(*) {
  animation: vt-fade-out 300ms var(--ease) both;
  mix-blend-mode: plus-lighter;
}
html:active-view-transition-type(theme)::view-transition-new(*) {
  animation: vt-fade-in 300ms var(--ease) both;
  mix-blend-mode: plus-lighter;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
