2026-10-27 TUE UTC 5:36 AM TPE 1:36 PM Auto-updates hourly 中文 EN
LIVE WIRE
← Back to the site changelog Site Changelog

Original commit record

Animate classic header wing logo on hover (#407)

SHA
b39fc6b3b9a0dd03f975f074cf30f2e9a81bb181
Commit date
Author
neo · 125815713+nnneeeooo-nnneeeooo@users.noreply.github.com
Committer
GitHub · noreply@github.com

Changed files (6)

  • pipeline/build.py modified · +1 / −0
  • static/logo-flight.js added · +185 / −0
  • static/site.css modified · +67 / −4
  • templates/base.html modified · +10 / −1
  • tests/test_logo_assets.py modified · +5 / −1
  • tests/test_logo_flight.js added · +112 / −0

Full diff

diff --git a/pipeline/build.py b/pipeline/build.py\nindex d9a152fe6..163571efe 100644\n--- a/pipeline/build.py\n+++ b/pipeline/build.py\n@@ -97,6 +97,7 @@ def static_asset_version() -> str:\n         "pixel.css",\n         "fusion-pixel-tw-v2026-10-16.woff2",\n         "app.js",\n+        "logo-flight.js",\n         "text-spacing.js",\n         "search.js",\n         "highlight.js",\ndiff --git a/static/logo-flight.js b/static/logo-flight.js\nnew file mode 100644\nindex 000000000..87f25c33a\n--- /dev/null\n+++ b/static/logo-flight.js\n@@ -0,0 +1,185 @@\n+/* A single, user-triggered flight for the approved classic wing mark. */\n+(function () {\n+  "use strict";\n+\n+  var link = document.querySelector("[data-brand-flight]");\n+  if (!link || !window.matchMedia || !window.requestAnimationFrame) return;\n+\n+  var sprite = link.querySelector("[data-brand-sprite]");\n+  var spriteImage = sprite && sprite.querySelector("img");\n+  var shell = link.closest(".brand-shell");\n+  var trailSvg = shell && shell.querySelector("[data-brand-flight-trail]");\n+  var trail = trailSvg && trailSvg.querySelector("path");\n+  if (!spriteImage || !trail) return;\n+\n+  var reduced = window.matchMedia("(prefers-reduced-motion: reduce)");\n+  var frame = 0;\n+  var runId = 0;\n+  var playing = false;\n+  var duration = 7350;\n+  var clamp = function (value) { return Math.min(1, Math.max(0, value)); };\n+  var ease = function (value) {\n+    var t = clamp(value);\n+    return t * t * (3 - 2 * t);\n+  };\n+  var lerp = function (a, b, t) { return a + (b - a) * t; };\n+  var cubic = function (a, b, c, d, t) {\n+    var q = 1 - t;\n+    return q*q*q*a + 3*q*q*t*b + 3*q*t*t*c + t*t*t*d;\n+  };\n+\n+  function stop() {\n+    runId++;\n+    if (frame) window.cancelAnimationFrame(frame);\n+    frame = 0;\n+    playing = false;\n+    link.classList.remove("is-flying");\n+    sprite.classList.remove("is-peeking", "is-landing");\n+    sprite.removeAttribute("data-gaze");\n+    sprite.style.transform = "";\n+    sprite.style.removeProperty("--brand-effects-opacity");\n+    trailSvg.style.opacity = "";\n+    trail.setAttribute("d", "");\n+  }\n+\n+  function play() {\n+    if (playing || reduced.matches || document.documentElement.dataset.theme === "8bit" ||\n+        !spriteImage.complete || !spriteImage.naturalWidth) return;\n+    stop();\n+\n+    var shellBox = shell.getBoundingClientRect();\n+    var linkBox = link.getBoundingClientRect();\n+    var spriteBox = sprite.getBoundingClientRect();\n+    if (!linkBox.width || !spriteBox.width) return;\n+\n+    var scale = linkBox.width / 320;\n+    var rightEdge = Math.min(shellBox.right, shellBox.left + 760);\n+    var topRightX = rightEdge - linkBox.left - spriteBox.width * .71 - 18;\n+    var topRightY = shellBox.top + Math.max(20 * scale, 16) - linkBox.top - spriteBox.height / 2;\n+    var topControlY = Math.min(-40 * scale, topRightY - 18 * scale);\n+    var lowerLeftX = Math.min(89 * scale, shellBox.width * .2);\n+    var lowerLeftY = linkBox.height + 33 * scale - spriteBox.height / 2;\n+    var centerX = linkBox.left - shellBox.left + spriteBox.width / 2;\n+    var centerY = linkBox.top - shellBox.top + spriteBox.height / 2;\n+    var points = [];\n+    var id = runId;\n+    var started = window.performance.now();\n+\n+    trailSvg.setAttribute("viewBox", "0 0 " + shellBox.width + " " + shellBox.height);\n+    link.classList.add("is-flying");\n+    playing = true;\n+\n+    function poseAt(ms) {\n+      var x, y, angle, size, peeking = false, gaze = "", landing = false, effects = 1;\n+      var u, turn;\n+      if (ms < 430) {\n+        u = ease(ms / 430);\n+        x = 9 * scale * u + Math.sin(u * Math.PI * 2) * 3 * scale;\n+        y = -18 * scale * u - Math.sin(u * Math.PI) * 8 * scale;\n+        angle = -22 * u;\n+        size = lerp(1, .42, u);\n+      } else if (ms < 1850) {\n+        u = ease((ms - 430) / 1420);\n+        x = cubic(9*scale, topRightX*.15, topRightX*.65, topRightX, u);\n+        y = cubic(-18*scale, topControlY, topRightY + 38*scale, topRightY, u);\n+        y += Math.sin(u * Math.PI * 4) * 9 * scale * Math.sin(u * Math.PI);\n+        angle = lerp(-22, 14, u) + Math.sin(u * Math.PI * 3) * 17;\n+        size = .42 + Math.sin(u * Math.PI * 2) * .025;\n+      } else if (ms < 2850) {\n+        u = ease((ms - 1850) / 1000);\n+        turn = u * Math.PI * 2;\n+        x = topRightX + 23 * scale * (Math.cos(turn) - 1);\n+        y = topRightY + 15 * scale * Math.sin(turn);\n+        angle = 14 + 360 * u;\n+        size = .42;\n+        gaze = u < .5 ? "right" : "left";\n+      } else if (ms < 3250) {\n+        u = (ms - 2850) / 400;\n+        var float = Math.sin(Math.PI * u);\n+        x = topRightX + Math.sin(u * Math.PI * 2) * float * 3 * scale;\n+        y = topRightY - float * 3 * scale;\n+        angle = 374 + Math.sin(u * Math.PI * 2) * float * 10;\n+        size = .42;\n+        peeking = true;\n+        gaze = u < .52 ? "right" : "left";\n+      } else if (ms < 4600) {\n+        u = ease((ms - 3250) / 1350);\n+        x = cubic(topRightX, topRightX + 16*scale, lowerLeftX + 170*scale, lowerLeftX, u);\n+        y = cubic(topRightY, topRightY + 103*scale, lowerLeftY - 43*scale, lowerLeftY, u);\n+        x += Math.sin(u * Math.PI * 3) * 9 * scale * Math.sin(u * Math.PI);\n+        angle = lerp(374, 546, u) + Math.sin(u * Math.PI * 2) * 12;\n+        size = .42;\n+      } else if (ms < 5350) {\n+        u = ease((ms - 4600) / 750);\n+        turn = u * Math.PI * 2;\n+        x = lowerLeftX + 16 * scale * Math.sin(2 * turn);\n+        y = lowerLeftY + 10 * scale * Math.sin(turn);\n+        angle = 546 + Math.sin(turn) * 28;\n+        size = .42;\n+        peeking = true;\n+        gaze = u < .5 ? "left" : "right";\n+      } else if (ms < 6600) {\n+        u = ease((ms - 5350) / 1250);\n+        x = cubic(lowerLeftX, lowerLeftX + 150*scale, -42*scale, 12*scale, u);\n+        y = cubic(lowerLeftY, lowerLeftY + 24*scale, -70*scale, -9*scale, u);\n+        y += Math.sin(u * Math.PI * 3) * 7 * scale * Math.sin(u * Math.PI);\n+        angle = lerp(546, 720, u);\n+        size = .42 + .30 * ease((u - .45) / .55);\n+      } else {\n+        var q = clamp((ms - 6600) / 750);\n+        u = ease(q);\n+        x = 12 * scale * (1 - u);\n+        y = -9 * scale * (1 - u);\n+        angle = 720 + Math.sin(q * Math.PI) * (1 - u) * 3;\n+        size = .72 + .28 * u + .022 * Math.sin(q * Math.PI) * u;\n+        effects = 1 - ease((q - .30) / .70);\n+        landing = true;\n+      }\n+      return { x: x, y: y, angle: angle, size: size, peeking: peeking,\n+        gaze: gaze, landing: landing, effects: effects };\n+    }\n+\n+    function draw(now) {\n+      if (id !== runId) return;\n+      var ms = Math.min(duration, now - started);\n+      var pose = poseAt(ms);\n+      sprite.style.transform = "translate(" + pose.x.toFixed(2) + "px, " +\n+        pose.y.toFixed(2) + "px) rotate(" + pose.angle.toFixed(2) +\n+        "deg) scale(" + pose.size.toFixed(3) + ")";\n+      sprite.classList.toggle("is-peeking", pose.peeking);\n+      sprite.classList.toggle("is-landing", pose.landing);\n+      sprite.style.setProperty("--brand-effects-opacity", pose.effects.toFixed(3));\n+      trailSvg.style.opacity = pose.effects.toFixed(3);\n+      if (pose.gaze) sprite.setAttribute("data-gaze", pose.gaze);\n+      else sprite.removeAttribute("data-gaze");\n+\n+      if (!pose.peeking) {\n+        points.push([centerX + pose.x, centerY + pose.y]);\n+        if (points.length > 30) points.shift();\n+      } else if (points.length > 1) points.shift();\n+      trail.setAttribute("d", points.length > 1\n+        ? "M " + points.map(function (point) {\n+          return point[0].toFixed(1) + " " + point[1].toFixed(1);\n+        }).join(" L ")\n+        : "");\n+      if (ms < duration) frame = window.requestAnimationFrame(draw);\n+      else stop();\n+    }\n+\n+    frame = window.requestAnimationFrame(draw);\n+  }\n+\n+  link.addEventListener("pointerenter", function (event) {\n+    if (event.pointerType === "mouse" || event.pointerType === "pen") play();\n+  });\n+  window.addEventListener("resize", stop);\n+  document.addEventListener("visibilitychange", function () {\n+    if (document.hidden) stop();\n+  });\n+  if (reduced.addEventListener) reduced.addEventListener("change", function () {\n+    if (reduced.matches) stop();\n+  });\n+  new MutationObserver(function () {\n+    if (document.documentElement.dataset.theme === "8bit") stop();\n+  }).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });\n+})();\ndiff --git a/static/site.css b/static/site.css\nindex ce0598ab5..248dea933 100644\n--- a/static/site.css\n+++ b/static/site.css\n@@ -65,14 +65,77 @@ a.ticker-item:hover .ticker-time { color: var(--color-accent); }\n .brand-link {\n   display: inline-flex; align-items: center; color: inherit;\n   text-decoration: none; flex: 0 0 320px; min-width: 250px;\n-  /* iOS in-app browsers briefly use the object's 220px HTML height while\n-     its external SVG is loading. Pin the frame ratio before that happens. */\n-  aspect-ratio: 50 / 11; overflow: hidden;\n+  /* Keep the logo frame stable while the SVG loads, with room for its flight. */\n+  aspect-ratio: 50 / 11; position: relative; overflow: visible;\n }\n .brand-logo {\n   display: block; width: 100%; height: 100%; max-width: 320px;\n   object-fit: contain; background: transparent;\n }\n+.brand-link.is-flying { z-index: 3; }\n+.brand-link.is-flying .brand-logo { clip-path: inset(0 0 0 23.5%); }\n+.brand-flight-sprite {\n+  position: absolute; top: 0; left: 0; width: 25%; height: 100%;\n+  visibility: hidden; pointer-events: none;\n+  transform-origin: 50% 50%; will-change: transform;\n+}\n+.brand-link.is-flying .brand-flight-sprite { visibility: visible; }\n+.brand-flight-crop { position: absolute; inset: 0; overflow: hidden; }\n+.brand-flight-crop img {\n+  display: block; width: 400%; height: 100%; max-width: none;\n+  object-fit: contain; object-position: left top;\n+}\n+.brand-flight-eyes {\n+  position: absolute; left: 39%; top: 39%; display: none;\n+  align-items: center; gap: 3px; pointer-events: none;\n+  opacity: var(--brand-effects-opacity, 1);\n+}\n+.brand-flight-eyes i {\n+  position: relative; display: block; width: 8px; height: 9px;\n+  border-radius: 50%; background: var(--color-bg);\n+  box-shadow: 0 0 0 1px var(--color-text);\n+}\n+.brand-flight-eyes i::after {\n+  content: ""; position: absolute; top: 3px; left: 3px;\n+  width: 3px; height: 3px; border-radius: 50%;\n+  background: var(--color-text); transition: transform .12s ease;\n+}\n+.brand-link.is-flying .brand-flight-eyes { display: flex; }\n+.brand-flight-sprite[data-gaze="right"] .brand-flight-eyes i::after { transform: translateX(3px); }\n+.brand-flight-sprite[data-gaze="left"] .brand-flight-eyes i::after { transform: translateX(-2px); }\n+.brand-flight-sprite.is-peeking .brand-flight-eyes i { animation: brand-flight-blink 1.1s ease-in-out infinite; }\n+.brand-flight-spark {\n+  position: absolute; display: none; width: 5px; height: 5px;\n+  border-radius: 50%; background: var(--color-accent-2);\n+  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-2) 16%, transparent);\n+  animation: brand-flight-twinkle .28s ease-in-out infinite alternate;\n+}\n+.brand-flight-spark-a { left: -7%; top: 36%; }\n+.brand-flight-spark-b { left: 9%; top: 83%; width: 4px; height: 4px; animation-delay: .13s; }\n+.brand-flight-spark-c { left: 42%; top: -8%; width: 3px; height: 3px; animation-delay: .23s; }\n+.brand-link.is-flying .brand-flight-spark { display: block; }\n+.brand-flight-sprite.is-landing .brand-flight-spark {\n+  animation: none; opacity: var(--brand-effects-opacity, 1);\n+}\n+.brand-flight-trail {\n+  position: absolute; inset: 0; display: block; width: 100%; height: 100%;\n+  pointer-events: none; overflow: visible; z-index: 2;\n+}\n+.brand-flight-trail path {\n+  fill: none; stroke: var(--color-accent-2); stroke-width: 2;\n+  stroke-linecap: round; stroke-dasharray: 2 7; opacity: .48;\n+}\n+@keyframes brand-flight-twinkle {\n+  from { opacity: .2; transform: scale(.6); }\n+  to { opacity: 1; transform: scale(1.2); }\n+}\n+@keyframes brand-flight-blink {\n+  0%, 42%, 52%, 100% { transform: scaleY(1); }\n+  47% { transform: scaleY(.12); }\n+}\n+@media (prefers-reduced-motion: reduce) {\n+  .brand-flight-spark, .brand-flight-eyes i { animation: none !important; }\n+}\n .footer-logo {\n   display: block; width: min(100%, 300px); height: auto;\n   background: transparent;\n@@ -145,7 +208,7 @@ a.ticker-item:hover .ticker-time { color: var(--color-accent); }\n .utility-date, .utility-clock { letter-spacing: .06em; }\n .utility-clock { font-weight: 600; }\n .utility-update { margin-left: auto; display: flex; align-items: center; gap: 7px; }\n-.brand-shell { border-bottom: 2px solid var(--color-divider); }\n+.brand-shell { position: relative; border-bottom: 2px solid var(--color-divider); }\n .brand-row {\n   padding: 8px 20px; display: flex; align-items: center;\n   gap: clamp(36px, 4vw, 56px);\ndiff --git a/templates/base.html b/templates/base.html\nindex 83210c08c..1b5627ffd 100644\n--- a/templates/base.html\n+++ b/templates/base.html\n@@ -63,6 +63,7 @@ gtag('config', '{{ ga4_measurement_id }}', {\n       title="{{ t.siteName }} — {{ t.latest }}" href="{{ feed_url }}">\n {% block head_extra %}{% endblock %}\n <script src="{{ base }}/assets/app.js?v={{ asset_version }}" defer></script>\n+<script src="{{ base }}/assets/logo-flight.js?v={{ asset_version }}" defer></script>\n <script src="{{ base }}/assets/text-spacing.js?v={{ asset_version }}" defer></script>\n </head>\n <body>\n@@ -129,13 +130,20 @@ gtag('config', '{{ ga4_measurement_id }}', {\n   </div>\n   <div class="brand-shell">\n     <div class="brand-row">\n-      <a class="brand-link" href="{{ home_url }}" aria-label="SKYTICAL — {{ t.homeLabel }}">\n+      <a class="brand-link" href="{{ home_url }}" aria-label="SKYTICAL — {{ t.homeLabel }}" data-brand-flight>\n         <object class="brand-logo" data="{{ base }}/assets/skytical-logo.svg?v={{ asset_version }}"\n                 type="image/svg+xml" aria-hidden="true" tabindex="-1"\n                 width="1000" height="220" style="pointer-events:none;">\n           <img src="{{ base }}/assets/skytical-logo.svg?v={{ asset_version }}"\n                alt="" width="1000" height="220">\n         </object>\n+        <span class="brand-flight-sprite" data-brand-sprite aria-hidden="true">\n+          <span class="brand-flight-crop"><img src="{{ base }}/assets/skytical-logo.svg?v={{ asset_version }}" alt="" width="1000" height="220"></span>\n+          <span class="brand-flight-eyes"><i></i><i></i></span>\n+          <span class="brand-flight-spark brand-flight-spark-a"></span>\n+          <span class="brand-flight-spark brand-flight-spark-b"></span>\n+          <span class="brand-flight-spark brand-flight-spark-c"></span>\n+        </span>\n       </a>\n       <a class="pixel-brand" href="{{ home_url }}" aria-label="SKYTICΛL — {{ t.homeLabel }}">\n         <img src="{{ base }}/assets/skytical-logo-8bit-dark.svg?v={{ asset_version }}"\n@@ -152,6 +160,7 @@ gtag('config', '{{ ga4_measurement_id }}', {\n         {% endfor %}\n       </nav>\n     </div>\n+    <svg class="brand-flight-trail" data-brand-flight-trail aria-hidden="true"><path></path></svg>\n   </div>\n   {% if ticker_items %}\n   <div class="ticker-shell">\ndiff --git a/tests/test_logo_assets.py b/tests/test_logo_assets.py\nindex cdf0729de..6de054d6b 100644\n--- a/tests/test_logo_assets.py\n+++ b/tests/test_logo_assets.py\n@@ -87,7 +87,11 @@ def main() -> None:\n     assert wing_top == red_top == lockup["wordmarkOrigin"][1]\n \n     assert png_dimensions(social_path) == (1200, 630)\n-    assert home.count(f'/assets/skytical-logo.svg?v={build.ASSET_VERSION}') >= 2\n+    assert home.count(f'/assets/skytical-logo.svg?v={build.ASSET_VERSION}') >= 3\n+    assert 'data-brand-flight' in home\n+    assert 'data-brand-sprite' in home\n+    assert f'/assets/logo-flight.js?v={build.ASSET_VERSION}' in home\n+    assert (ROOT / "site" / "assets" / "logo-flight.js").is_file()\n     assert f'/assets/skytical-mark.svg?v={build.ASSET_VERSION}' in home\n     assert f'/assets/skytical-social.png?v={build.ASSET_VERSION}' in home\n     assert "/assets/logo-fix.css" not in home\ndiff --git a/tests/test_logo_flight.js b/tests/test_logo_flight.js\nnew file mode 100644\nindex 000000000..6333525eb\n--- /dev/null\n+++ b/tests/test_logo_flight.js\n@@ -0,0 +1,112 @@\n+/* Verify the header logo's hover journey without a browser dependency. */\n+const assert = require("node:assert/strict");\n+const fs = require("node:fs");\n+const path = require("node:path");\n+const vm = require("node:vm");\n+\n+const source = fs.readFileSync(path.join(__dirname, "../static/logo-flight.js"), "utf8");\n+\n+function setup(width, reducedMotion = false, theme = "light") {\n+  let now = 0;\n+  let next = null;\n+  function element(rect = {}) {\n+    const listeners = {};\n+    const attributes = {};\n+    const classes = new Set();\n+    const style = {\n+      setProperty(name, value) { this[name] = value; },\n+      removeProperty(name) { delete this[name]; },\n+    };\n+    return {\n+      listeners, attributes, style,\n+      classList: {\n+        add(...names) { names.forEach(name => classes.add(name)); },\n+        remove(...names) { names.forEach(name => classes.delete(name)); },\n+        toggle(name, enabled) { if (enabled) classes.add(name); else classes.delete(name); },\n+        contains(name) { return classes.has(name); },\n+      },\n+      addEventListener(name, handler) { listeners[name] = handler; },\n+      setAttribute(name, value) { attributes[name] = value; },\n+      removeAttribute(name) { delete attributes[name]; },\n+      getBoundingClientRect() { return rect; },\n+    };\n+  }\n+  const brandWidth = Math.min(320, width - 36);\n+  const brandHeight = brandWidth * .22;\n+  const shell = element({ left: 0, top: 0, right: width, bottom: 86, width, height: 86 });\n+  const link = element({ left: 18, top: 8, right: 18 + brandWidth,\n+    bottom: 8 + brandHeight, width: brandWidth, height: brandHeight });\n+  const sprite = element({ left: 18, top: 8, right: 18 + brandWidth / 4,\n+    bottom: 8 + brandHeight, width: brandWidth / 4, height: brandHeight });\n+  const trailSvg = element();\n+  const trail = element();\n+  link.querySelector = () => sprite;\n+  sprite.querySelector = () => ({ complete: true, naturalWidth: 1000 });\n+  link.closest = () => shell;\n+  shell.querySelector = () => trailSvg;\n+  trailSvg.querySelector = () => trail;\n+  const root = { dataset: { theme } };\n+  const document = {\n+    documentElement: root, hidden: false, listeners: {},\n+    querySelector: () => link,\n+    addEventListener(name, handler) { this.listeners[name] = handler; },\n+  };\n+  const reduced = { matches: reducedMotion, addEventListener() {} };\n+  const window = {\n+    matchMedia: () => reduced,\n+    performance: { now: () => now },\n+    requestAnimationFrame(handler) { next = handler; return 1; },\n+    cancelAnimationFrame() { next = null; },\n+    listeners: {},\n+    addEventListener(name, handler) { this.listeners[name] = handler; },\n+  };\n+  vm.runInNewContext(source, { document, window, MutationObserver: class {\n+    observe() {}\n+  } });\n+  return {\n+    link, sprite, trail, root, window, document,\n+    hover(pointerType = "mouse") { link.listeners.pointerenter({ pointerType }); },\n+    tick(time) {\n+      now = time;\n+      assert.ok(next, "animation ended before " + time);\n+      next(time);\n+      const match = (sprite.style.transform || "").match(\n+        /translate\(([-\d.]+)px, ([-\d.]+)px\) rotate\(([-\d.]+)deg\) scale\(([-\d.]+)\)/\n+      );\n+      return match && { x: +match[1], y: +match[2], angle: +match[3], size: +match[4] };\n+    },\n+  };\n+}\n+\n+for (const width of [320, 736, 1200]) {\n+  const ui = setup(width);\n+  ui.hover("touch");\n+  assert.equal(ui.link.classList.contains("is-flying"), false);\n+  ui.hover();\n+  assert.equal(ui.link.classList.contains("is-flying"), true);\n+  const tiny = ui.tick(430);\n+  assert.ok(Math.abs(tiny.size - .42) < .002);\n+  const top = ui.tick(1850);\n+  const orbit = ui.tick(2350);\n+  assert.ok(Math.abs(top.x - orbit.x) > 10, "upper corner must have a visible loop");\n+  ui.tick(4600);\n+  const lower = ui.tick(4788);\n+  const lowerOrbit = ui.tick(4975);\n+  assert.ok(Math.abs(lower.x - lowerOrbit.x) > 5, "lower corner must have a visible loop");\n+  ui.tick(6600);\n+  const nearHome = ui.tick(7349);\n+  assert.ok(Math.abs(nearHome.x) < .1 && Math.abs(nearHome.y) < .1);\n+  assert.ok(Math.abs(nearHome.size - 1) < .003, "landing must reach original size smoothly");\n+  ui.tick(7350);\n+  assert.equal(ui.link.classList.contains("is-flying"), false);\n+  assert.equal(ui.sprite.style.transform, "");\n+  assert.equal(ui.trail.attributes.d, "");\n+}\n+\n+for (const [reducedMotion, theme] of [[true, "light"], [false, "8bit"]]) {\n+  const ui = setup(736, reducedMotion, theme);\n+  ui.hover();\n+  assert.equal(ui.link.classList.contains("is-flying"), false);\n+}\n+\n+console.log("test_logo_flight: OK");

You found a secret theme!

8-bit mode is unlocked. Refresh to apply it now. You can switch themes anytime using the theme selector.