diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..982ef31 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,37 @@ +name: Deploy GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload site + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/AGENTS.md b/AGENTS.md index 3d15630..d882646 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ macOS Tahoe 26-only Swift Package (SPM executable → SwiftUI app) that imports - Version/build is in **AppVersion.swift** (single source; `Scripts/build-app.sh` parses it for Info.plist — update **both** `shortVersion` and `buildNumber` or the script exits 1). - Release order matters: in one commit, bump AppVersion.swift **and** add the `## ` section to CHANGELOG.md, push it, then push tag `v` pointing at that commit. release.yml's changelog extraction exits 1 if the section is missing, and the Info.plist version comes from AppVersion.swift — tagging a commit without either publishes the wrong version or fails the run (a moved tag is the only recovery). Release zips `dist/AerialDrop.app` as `AerialDrop--macOS.zip`. +- The Pages site hardcodes the current version as a no-JS/offline fallback: bump `v1.1.3` in `docs/index.html` (hero terminal output, `#expectVersion`, unzip commands, release chip) and the `applyRelease("v1.1.3", …)` fallback in `docs/app.js` in the same commit as AppVersion.swift, or visitors whose release fetch fails see a stale version. `Assets/icon.svg` and `docs/assets/icon.svg` must stay byte-identical, and `docs/assets/icon.png` must be regenerated from it when the art changes. ## MCP Tools: code-review-graph diff --git a/Assets/icon.svg b/Assets/icon.svg new file mode 100644 index 0000000..21db64e --- /dev/null +++ b/Assets/icon.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index d7386f9..29bb5c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # AerialDrop [![CI](https://github.com/YapWH1208/AerialDrop/actions/workflows/ci.yml/badge.svg)](https://github.com/YapWH1208/AerialDrop/actions/workflows/ci.yml) +[![Website](https://img.shields.io/badge/website-yapwh1208.github.io%2FAerialDrop-0a84ff)](https://yapwh1208.github.io/AerialDrop/) AerialDrop imports your own videos into macOS Tahoe's native Aerial (wallpaper) catalogue, so custom videos play as screen savers and lock screen wallpapers using Apple's own playback pipeline — no helper processes, no app-managed player. @@ -93,6 +94,7 @@ Every manifest write is backed up automatically first: backups live under `aeria ## Documentation +- [Website](https://yapwh1208.github.io/AerialDrop/) — interactive landing page (source: `docs/`, published to GitHub Pages) - [ARCHITECTURE.md](ARCHITECTURE.md) — processing pipeline - [TESTING.md](TESTING.md) — manual test procedure - [CHANGELOG.md](CHANGELOG.md) — release history diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/app.js b/docs/app.js new file mode 100644 index 0000000..e352688 --- /dev/null +++ b/docs/app.js @@ -0,0 +1,392 @@ +/* AerialDrop landing page — interactivity (vanilla JS, no dependencies) */ +(function () { + "use strict"; + + var $ = function (sel, root) { return (root || document).querySelector(sel); }; + var $$ = function (sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }; + var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + /* ---------- Theme ---------- */ + var themeToggle = $("#themeToggle"); + function applyTheme(theme) { + document.documentElement.setAttribute("data-theme", theme); + var meta = $('meta[name="theme-color"]'); + if (meta) meta.setAttribute("content", theme === "light" ? "#f3f6f9" : "#070b10"); + if (themeToggle) themeToggle.setAttribute("aria-pressed", theme === "light" ? "true" : "false"); + try { localStorage.setItem("ad-theme", theme); } catch (e) {} + } + if (themeToggle) { + themeToggle.addEventListener("click", function () { + var current = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark"; + applyTheme(current === "light" ? "dark" : "light"); + }); + applyTheme(document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark"); + } + + /* ---------- Mobile nav ---------- */ + var navToggle = $("#navToggle"), navLinks = $("#navLinks"); + if (navToggle && navLinks) { + navToggle.addEventListener("click", function () { + var open = navLinks.classList.toggle("open"); + navToggle.setAttribute("aria-expanded", open ? "true" : "false"); + }); + $$("a", navLinks).forEach(function (a) { + a.addEventListener("click", function () { + navLinks.classList.remove("open"); + navToggle.setAttribute("aria-expanded", "false"); + }); + }); + } + + /* ---------- Active nav link while scrolling ---------- */ + var navAnchors = {}; + $$(".nav__links a[href^='#']").forEach(function (a) { navAnchors[a.getAttribute("href").slice(1)] = a; }); + var sectionIds = Object.keys(navAnchors); + if (sectionIds.length && "IntersectionObserver" in window) { + var ioNav = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + Object.keys(navAnchors).forEach(function (id) { + navAnchors[id].classList.toggle("is-active", id === entry.target.id); + }); + } + }); + }, { rootMargin: "-40% 0px -55% 0px" }); + sectionIds.forEach(function (id) { + var el = document.getElementById(id); + if (el) ioNav.observe(el); + }); + } + + /* ---------- Clipboard helpers ---------- */ + function legacyCopy(text) { + var ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand("copy"); } catch (e) {} + document.body.removeChild(ta); + return ok; + } + function copyText(text, btn, doneText) { + function flash(label, cls) { + if (!btn) return; + var original = btn.textContent; + btn.textContent = label; + btn.classList.add(cls); + setTimeout(function () { + btn.textContent = original; + btn.classList.remove(cls); + }, 1600); + } + var okLabel = doneText || "Copied \u2713"; + function tryLegacy() { + if (legacyCopy(text)) { + flash(okLabel, "is-copied"); + } else { + flash("Copy failed", "is-failed"); + } + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then( + function () { flash(okLabel, "is-copied"); }, + tryLegacy + ); + } else { + tryLegacy(); + } + } + function commandFor(btn) { + var attr = btn.getAttribute("data-copy"); + if (attr) return attr.replace(/\\n/g, "\n"); + var code = btn.parentElement ? btn.parentElement.querySelector("code") : null; + return code ? code.innerText : ""; + } + $$(".cmd__copy, .term-copy").forEach(function (btn) { + btn.addEventListener("click", function () { + copyText(commandFor(btn), btn); + }); + }); + $$(".term-line--cmd").forEach(function (line) { + line.addEventListener("click", function (e) { + if (e.target.closest(".term-copy")) return; + var btn = line.querySelector(".term-copy"); + if (btn) copyText(commandFor(btn), btn); + }); + }); + + /* ---------- Latest release (GitHub API, graceful fallback) ---------- */ + function humanSize(bytes) { + if (typeof bytes !== "number" || !isFinite(bytes) || bytes <= 0) return "\u2014"; + var units = ["B", "KB", "MB", "GB"], i = 0; + while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; } + return bytes.toFixed(i > 1 ? 1 : 0) + " " + units[i]; + } + function applyRelease(tag, downloadUrl) { + var set = function (id, text) { var el = document.getElementById(id); if (el) el.textContent = text; }; + set("releaseTag", tag); + set("expectVersion", tag.replace(/^v/, "")); + var version = tag.replace(/^v/, ""); + var zipName = "AerialDrop-" + version + "-macOS.zip"; + var unzipCmd = $("#unzipCmd"); + if (unzipCmd) unzipCmd.textContent = "unzip -q " + zipName + " -d /Applications"; + var unzipCopy = $("#unzipCopy"); + if (unzipCopy) unzipCopy.setAttribute("data-copy", "unzip -q " + zipName + " -d /Applications"); + var out1 = $("#termOut1"); + if (out1) out1.innerHTML = '==> Downloading ' + zipName; + var out4 = $("#termOut4"); + if (out4) out4.innerHTML = '\u2713 aerialdrop ' + version + " is ready. Open it, drop in a video."; + if (downloadUrl) { + $$("[data-download]").forEach(function (el) { el.setAttribute("href", downloadUrl); }); + $$("[data-download-label]").forEach(function (el) { el.textContent = "Download AerialDrop " + tag; }); + } + } + function fetchRelease() { + var url = "https://api.github.com/repos/YapWH1208/AerialDrop/releases/latest"; + var controller = ("AbortController" in window) ? new AbortController() : null; + var timer = controller ? setTimeout(function () { controller.abort(); }, 6000) : null; + fetch(url, controller ? { signal: controller.signal, headers: { Accept: "application/vnd.github+json" } } : {}) + .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); }) + .then(function (rel) { + var tag = rel.tag_name || "v1.1.3"; + var asset = (rel.assets || []).filter(function (a) { return /macOS/i.test(a.name) && /\.zip$/i.test(a.name); })[0]; + var set = function (id, text) { var el = document.getElementById(id); if (el) el.textContent = text; }; + set("releaseSize", humanSize(asset && asset.size)); + applyRelease(tag, asset && asset.browser_download_url); + }) + .catch(function () { + var set = function (id, text) { var el = document.getElementById(id); if (el) el.textContent = text; }; + set("releaseSize", "unavailable offline"); + applyRelease("v1.1.3", null); + }) + .then(function () { if (timer) clearTimeout(timer); }); + } + if (window.fetch) fetchRelease(); + + /* ---------- Hero terminal typing ---------- */ + var termCmd = $("#termCmd"); + if (termCmd) { + var cmdText = termCmd.getAttribute("data-text") || + "brew install --cask yapwh1208/tap/aerialdrop"; + var termOuts = ["termOut1", "termOut2", "termOut3", "termOut4", "termCursor"].map(function (id) { + return document.getElementById(id); + }); + var started = false, typing = false, step = 0, charIndex = 0; + + function typeStep() { + if (charIndex < cmdText.length) { + termCmd.textContent = cmdText.slice(0, charIndex + 1); + charIndex++; + setTimeout(typeStep, 26); + } else { + typing = false; + setTimeout(function () { revealNext(); }, 550); + } + } + function revealNext() { + if (step < termOuts.length) { + var el = termOuts[step]; + if (el) el.classList.remove("is-hidden"); + step++; + setTimeout(revealNext, step === termOuts.length ? 900 : 620); + } else { + var copyBtn = document.querySelector(".term-line--cmd .term-copy"); + if (copyBtn) copyBtn.classList.add("is-visible"); + } + } + function startTerminal() { + if (started) return; + started = true; + if (reduced) { + termCmd.textContent = cmdText; + termOuts.forEach(function (el) { if (el) el.classList.remove("is-hidden"); }); + var copyBtn2 = document.querySelector(".term-line--cmd .term-copy"); + if (copyBtn2) copyBtn2.classList.add("is-visible"); + return; + } + typing = true; + setTimeout(typeStep, 700); + } + if ("IntersectionObserver" in window) { + var ioTerm = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + startTerminal(); + ioTerm.disconnect(); + } + }); + }, { threshold: 0.3 }); + var termBody = $("#termBody"); + if (termBody) ioTerm.observe(termBody); + } else { + startTerminal(); + } + } + + /* ---------- Install method tabs ---------- */ + var installer = $("#installer"); + if (installer) { + var methods = $$(".method", installer); + var panels = $$(".panel", installer); + function selectMethod(method) { + methods.forEach(function (m) { + var on = m === method; + m.classList.toggle("is-active", on); + m.setAttribute("aria-selected", on ? "true" : "false"); + m.setAttribute("tabindex", on ? "0" : "-1"); + }); + panels.forEach(function (p) { + var on = p.id === method.getAttribute("data-tab"); + p.classList.toggle("is-active", on); + p.hidden = !on; + }); + } + methods.forEach(function (method) { + method.addEventListener("click", function () { selectMethod(method); }); + method.addEventListener("keydown", function (e) { + var idx = methods.indexOf(method); + if (e.key === "ArrowRight" || e.key === "ArrowDown") { + e.preventDefault(); + selectMethod(methods[(idx + 1) % methods.length]); + methods[(idx + 1) % methods.length].focus(); + } + if (e.key === "ArrowLeft" || e.key === "ArrowUp") { + e.preventDefault(); + selectMethod(methods[(idx - 1 + methods.length) % methods.length]); + methods[(idx - 1 + methods.length) % methods.length].focus(); + } + }); + }); + } + + /* ---------- Pipeline stepper ---------- */ + var stepperEl = $("#stepper"); + if (stepperEl) { + var nodes = $$(".stepper__node", stepperEl); + var panelsEl = $$(".step-panel"); + var progress = $("#stepProgress"), status = $("#stepStatus"); + var prevBtn = $("#stepPrev"), nextBtn = $("#stepNext"), playBtn = $("#stepPlay"); + var titles = nodes.map(function (n) { return n.textContent.trim().replace(/^\d+/, "").trim(); }); + var current = 0, autoplay = false, timer = null, STEP_MS = 3400; + + function render() { + nodes.forEach(function (n, i) { + var li = n.parentElement; + li.classList.toggle("is-active", i === current); + li.classList.toggle("is-done", i < current); + if (i === current) n.setAttribute("aria-current", "step"); else n.removeAttribute("aria-current"); + }); + panelsEl.forEach(function (p, i) { p.classList.toggle("is-active", i === current); p.hidden = i !== current; }); + if (progress) progress.style.width = ((current + 1) / nodes.length * 100) + "%"; + if (status) status.textContent = "Step " + (current + 1) + " of " + nodes.length + " \u00b7 " + titles[current]; + if (prevBtn) prevBtn.disabled = current === 0; + if (nextBtn) nextBtn.disabled = current === nodes.length - 1; + } + function go(i) { + current = Math.max(0, Math.min(nodes.length - 1, i)); + render(); + } + function stopAutoplay() { + autoplay = false; + if (timer) { clearInterval(timer); timer = null; } + if (playBtn) { playBtn.textContent = "Play"; playBtn.setAttribute("aria-pressed", "false"); } + } + function startAutoplay() { + if (reduced) return; + autoplay = true; + playBtn.textContent = "Pause"; + playBtn.setAttribute("aria-pressed", "true"); + timer = setInterval(function () { + if (current >= nodes.length - 1) { go(0); } else { go(current + 1); } + }, STEP_MS); + } + nodes.forEach(function (n, i) { + n.addEventListener("click", function () { stopAutoplay(); go(i); }); + }); + if (prevBtn) prevBtn.addEventListener("click", function () { stopAutoplay(); go(current - 1); }); + if (nextBtn) nextBtn.addEventListener("click", function () { stopAutoplay(); go(current + 1); }); + if (playBtn) playBtn.addEventListener("click", function () { autoplay ? stopAutoplay() : startAutoplay(); }); + if (document.addEventListener) { + document.addEventListener("visibilitychange", function () { if (document.hidden) stopAutoplay(); }); + } + render(); + } + + /* ---------- First-run checklist (persisted) ---------- */ + var checkBoxes = $$("#checklistBox .check input[type='checkbox']"); + if (checkBoxes.length) { + var KEY = "ad-checklist-v1"; + var countEl = $("#checkCount"), doneEl = $("#checkDone"), resetBtn = $("#checkReset"); + var barEl = $("#checkProgress"); + var saved = null; + try { saved = JSON.parse(localStorage.getItem(KEY) || "null"); } catch (e) {} + function renderChecklist() { + var done = 0; + checkBoxes.forEach(function (box) { + var li = box.closest(".check"); + if (box.checked) { + done++; + if (li) li.classList.add("is-done"); + } else if (li) { + li.classList.remove("is-done"); + } + }); + if (countEl) countEl.textContent = done + " of " + checkBoxes.length + " done"; + if (barEl) barEl.style.width = (done / checkBoxes.length * 100) + "%"; + if (doneEl) doneEl.classList.toggle("is-hidden", done < checkBoxes.length); + } + checkBoxes.forEach(function (box, i) { + if (saved && saved[i]) box.checked = true; + box.addEventListener("change", function () { + var values = checkBoxes.map(function (b) { return b.checked; }); + try { localStorage.setItem(KEY, JSON.stringify(values)); } catch (e) {} + renderChecklist(); + }); + }); + if (resetBtn) { + resetBtn.addEventListener("click", function () { + checkBoxes.forEach(function (box) { box.checked = false; }); + try { localStorage.removeItem(KEY); } catch (e) {} + renderChecklist(); + }); + } + renderChecklist(); + } + + /* ---------- OS compatibility check ---------- */ + var osEl = $("#osCheck"); + if (osEl) { + var ua = navigator.userAgent || ""; + var mac = /Macintosh|Mac OS X/i.test(ua) && !/iPhone|iPad|iPod/i.test(ua); + if (mac) { + osEl.innerHTML = '\u2713 macOS detected \u2014 need macOS 26 (Tahoe) or later'; + } else if (/iPhone|iPad|iPod/i.test(ua)) { + osEl.innerHTML = 'iOS detected \u2014 AerialDrop is a macOS app'; + } else { + osEl.innerHTML = 'Not macOS? \u2014 AerialDrop runs on macOS 26+'; + } + } + + /* ---------- Scroll reveal ---------- */ + var revealEls = $$(".reveal"); + if (revealEls.length && "IntersectionObserver" in window && !reduced) { + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add("in"); + io.unobserve(entry.target); + } + }); + }, { threshold: 0.12, rootMargin: "0px 0px -6% 0px" }); + revealEls.forEach(function (el) { io.observe(el); }); + } else { + revealEls.forEach(function (el) { el.classList.add("in"); }); + } + + /* ---------- Footer year ---------- */ + var yearEl = $("#year"); + if (yearEl) yearEl.textContent = String(new Date().getFullYear()); +})(); diff --git a/docs/assets/icon.png b/docs/assets/icon.png new file mode 100644 index 0000000..4ca90ac Binary files /dev/null and b/docs/assets/icon.png differ diff --git a/docs/assets/icon.svg b/docs/assets/icon.svg new file mode 100644 index 0000000..21db64e --- /dev/null +++ b/docs/assets/icon.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..337fd85 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,596 @@ + + + + + +AerialDrop — your videos, natively on the macOS Tahoe desktop + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

macOS Tahoe 26Free & open sourceMIT

+

Your videos, natively on the Tahoe desktop.

+

AerialDrop imports your MP4 or MOV into macOS Tahoe’s own Aerial catalogue. Screen saver and lock screen, played by Apple’s pipeline. No helper processes, no app-managed player.

+ +

One command. Copy it below or pick your method.

+
+ +
+ + +
+ v1.1.3 + checking… · macOS 26+ · download +
+
+
+
+ +
+
+
Encoding
HEVC Main10 · 30 fps · 10-bit Rec.709
+
Loops
80 s · sync samples at boundaries
+
Sources
MP4 / MOV · H.264 / HEVC
+
Backups
Auto, before every write
+
License
MIT
+
+
+ + +
+
+
+

Built like Apple’s own aerials

+

Every import is re-encoded to the exact sample-group shape Tahoe expects, then registered as a first-class catalogue entry.

+
+
+
+ Catalogue +

Native entries

+

Imports appear under AerialDrop in System Settings → Wallpaper, with HEIF previews like the built-in aerials.

+
+
+ Encoding +

HEVC Main10

+

30 fps, 10-bit full-range YUV, Rec.709, temporal sub-layers with a 15 fps base layer and 1.9 s closed GOPs.

+
+
+ Looping +

Every boundary safe

+

Short sources are repeated by passthrough export with the loop snapped to whole 30 fps frames, so every loop lands on a sync sample.

+
+
+ Activation +

One click

+

Imported aerials apply across all Spaces and displays; WallpaperAgent and WallpaperAerialsExtension are restarted and the change verified.

+
+
+ Safety +

Reversible writes

+

Every catalogue write is backed up first, compares the file before writing, and can be restored from the Maintenance menu.

+
+
+ Source +

Never touched

+

Your file is opened read-only. Crop, quality, and resolution are tuned in place with a live 16:9 preview mask.

+
+
+
+
+ + +
+
+
+
+

Install

+

Four ways in. Copy a command, run it, then verify with the check step.

+
+
checking your platform…
+
+ +
+
+ + + + +
+ +
+ +
+
+

Homebrew cask

+

The cask tracks new releases, so brew upgrade --cask aerialdrop keeps you current.

+
+
    +
  1. + +
    +

    Install the cask

    +
    +
    brew install --cask yapwh1208/tap/aerialdrop
    + +
    +
    +
  2. +
  3. + +
    +

    Verify it’s in place

    +
    +
    ls /Applications/AerialDrop.app
    + +
    +

    Expected output: AerialDrop.app and the version: +

    +
    +
    defaults read /Applications/AerialDrop.app/Contents/Info CFBundleShortVersionString
    + +
    +

    Expected output: 1.1.3

    +
    +
  4. +
  5. + +
    +

    To upgrade later

    +
    +
    brew update && brew upgrade --cask aerialdrop
    + +
    +
    +
  6. +
+
+ + + + + + + + + +
+
+ +
+ Before you install: the unsigned-app notice +

AerialDrop is ad-hoc signed and not notarized by Apple. The install methods above clear the macOS download quarantine, which disables Apple’s malware check for this app; you are trusting the publisher instead of Apple. Only install from the official + YapWH1208/AerialDrop repository, and audit the open-source code if you have concerns. The only way to get Apple’s own verification is Developer ID notarization, which needs a paid Apple Developer account.

+
+
+
+ + +
+
+
+

What happens when you import

+

Seven steps, one pipeline. Press play to watch it run, or step through it manually.

+
+ +
+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
  6. +
  7. +
+ +
+
+

Choose your video

+

Pick any MP4 or MOV (H.264 or HEVC), or drop it straight into the Import pane. You can tune the name, crop, quality, and output resolution before anything is written.

+
  • MP4 / MOV
  • H.264 / HEVC
  • Any orientation
+
+ + + + + + + +
+ +
+ + + +
+

Step 1 of 7 · Choose

+
+
+
+
+
+ + +
+
+
+

First wallpaper in five checks

+

Tick each step as you go. Your progress is saved in this browser.

+
+ +
+ +

0 of 5 done

+
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
  7. + + +
  8. +
  9. + + +
  10. +
+ + +
+
+
+ + +
+
+
+

Questions, answered

+
+
+
+ Does AerialDrop modify my original video? +

No. Your source is opened read-only and never touched. A re-encoded copy is installed into the wallpaper catalogue.

+
+
+ Why does my video need re-encoding at all? +

Tahoe’s native player expects HEVC Main10 with temporal sub-layers (a 15 fps base layer at 30 fps) and sync samples at loop boundaries. Without that shape, the unlock transition can fail with VideoSampleReadingErrors and leave a black desktop. AerialDrop encodes to exactly the shape Apple’s own aerials use.

+
+
+ My video is shorter than 80 seconds. Will it work? +

Yes. Short sources are encoded once, then repeated via passthrough export. The loop duration is snapped to whole 30 fps frames so every loop boundary lands on a sync sample.

+
+
+ Is the app signed or notarized? +

It is ad-hoc signed and not notarized. Gatekeeper may complain on first launch: right-click and choose Open once, or clear the quarantine. Only install from the official repository, and audit the open-source code if you have concerns.

+
+
+ Does AerialDrop need to keep running after setup? +

No. Once a wallpaper is imported, macOS plays it natively. AerialDrop is only needed for importing and managing the catalogue.

+
+
+ Where are my wallpapers stored? +

Inside ~/Library/Application Support/com.apple.wallpaper/aerials/, with backups under aerials/AerialDropBackups and Store/AerialDropBackups. Use Open Aerial Storage Folder from the Maintenance menu to get there.

+
+
+ What if a future macOS update changes the catalogue format? +

The catalogue is a private API, so it could change. Every write is backed up first, Restore Latest Backup can put the catalogue back, and a future update may require an AerialDrop update.

+
+
+
+
+ + +
+
+

Bring your own skies.

+

Turn a flight, a hike, a city at night, anything you shot, into a living Tahoe wallpaper.

+ +
+
+
+ + + + + + diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..c4fc81b --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,1651 @@ +/* ============================================================ + AerialDrop — landing page styles + Night / day themes via [data-theme] tokens on . + Radius rule: panels 14px, controls 9px, pills 999px. + ============================================================ */ + +:root { + --font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif; + --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + + --radius-panel: 14px; + --radius-control: 9px; + --radius-pill: 999px; + + --container: 1120px; + --ease: cubic-bezier(0.16, 1, 0.3, 1); + --ease-fast: cubic-bezier(0.33, 0, 0.2, 1); +} + +/* Dark is the default so the page renders even before JS sets data-theme. */ +:root, +html[data-theme="dark"] { + color-scheme: dark; + --bg: #070b10; + --bg-elev: #0b1219; + --bg-elev-2: #0e1722; + --line: rgba(150, 185, 215, 0.14); + --line-strong: rgba(150, 185, 215, 0.26); + --text: #e9eff5; + --text-dim: #93a4b6; + --text-faint: #67798d; + --accent: #6fb6f0; + --accent-ink: #071018; + --accent-soft: rgba(111, 182, 240, 0.12); + --accent-soft-strong: rgba(111, 182, 240, 0.2); + --ok: #5fbf8a; + --warn: #d9a35e; + --danger: #d16b6b; + --header-bg: rgba(7, 11, 16, 0.82); + --shadow: 0 18px 50px rgba(0, 0, 0, 0.45); + --scrollbar-thumb: rgba(150, 185, 215, 0.25); +} + +html[data-theme="light"] { + color-scheme: light; + --bg: #f3f6f9; + --bg-elev: #ffffff; + --bg-elev-2: #0d1520; + --line: rgba(16, 36, 56, 0.12); + --line-strong: rgba(16, 36, 56, 0.24); + --text: #0e1620; + --text-dim: #54636f; + --text-faint: #7c8b99; + --accent: #136fc1; + --accent-ink: #ffffff; + --accent-soft: rgba(19, 111, 193, 0.09); + --accent-soft-strong: rgba(19, 111, 193, 0.16); + --ok: #2e8b5f; + --warn: #a86a17; + --danger: #c24b4b; + --header-bg: rgba(243, 246, 249, 0.85); + --shadow: 0 18px 50px rgba(16, 36, 56, 0.12); + --scrollbar-thumb: rgba(16, 36, 56, 0.25); +} + +/* The hero terminal stays night-colored in both themes. */ +:root { + --term-bg: #080d13; + --term-text: #c9d6e2; + --term-dim: #6b7e90; + --term-accent: #7cc2f2; + --term-line: rgba(150, 185, 215, 0.14); + --term-ok: #7fd0a0; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 84px; +} + +body { + font-family: var(--font-ui); + font-size: 16px; + line-height: 1.6; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +::selection { + background: var(--accent-soft-strong); + color: var(--text); +} + +::-webkit-scrollbar { + width: 12px; +} +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 999px; + border: 3px solid transparent; + background-clip: content-box; +} +::-webkit-scrollbar-track { + background: transparent; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 4px; +} + +.mono { + font-family: var(--font-mono); +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + clip: rect(0 0 0 0); + overflow: hidden; + white-space: nowrap; +} + +.is-hidden { + display: none !important; +} + +.skip-link { + position: fixed; + top: -60px; + left: 16px; + z-index: 100; + padding: 10px 16px; + background: var(--accent); + color: var(--accent-ink); + border-radius: var(--radius-control); + font-weight: 600; + text-decoration: none; + transition: top 0.2s var(--ease-fast); +} +.skip-link:focus { + top: 12px; +} + +.container { + max-width: var(--container); + margin: 0 auto; + padding-inline: 24px; +} + +.container--narrow { + max-width: 840px; +} + +/* ---------------- Header / nav ---------------- */ + +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: var(--header-bg); + -webkit-backdrop-filter: blur(18px) saturate(1.4); + backdrop-filter: blur(18px) saturate(1.4); + border-bottom: 1px solid var(--line); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + height: 60px; + gap: 16px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--text); + font-weight: 700; + font-size: 17px; + letter-spacing: -0.01em; +} + +.brand__icon { + width: 28px; + height: 28px; + border-radius: 7px; +} + +.nav__links { + display: flex; + align-items: center; + gap: 4px; + list-style: none; +} + +.nav__links a { + display: inline-block; + padding: 8px 12px; + color: var(--text-dim); + text-decoration: none; + font-size: 14px; + font-weight: 500; + border-radius: var(--radius-control); + transition: color 0.2s var(--ease-fast), background-color 0.2s var(--ease-fast); +} + +.nav__links a:hover { + color: var(--text); + background: var(--accent-soft); +} + +.nav__links a.is-active { + color: var(--text); +} + +.nav__actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: 12px; +} + +.nav__toggle { + display: none; + flex-direction: column; + justify-content: center; + gap: 4px; + width: 40px; + height: 40px; + padding: 8px; + background: none; + border: 1px solid var(--line); + border-radius: var(--radius-control); + cursor: pointer; +} + +.nav__toggle-bar { + height: 2px; + background: var(--text); + border-radius: 2px; +} + +/* ---------------- Buttons ---------------- */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 18px; + border-radius: var(--radius-control); + border: 1px solid transparent; + font-family: var(--font-ui); + font-size: 15px; + font-weight: 600; + text-decoration: none; + cursor: pointer; + white-space: nowrap; + transition: transform 0.15s var(--ease-fast), box-shadow 0.2s var(--ease-fast), + background-color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), + color 0.2s var(--ease-fast); +} + +.btn .icon { + width: 18px; + height: 18px; + flex: none; +} + +.btn:active { + transform: scale(0.97); +} + +.btn--primary { + background: var(--accent); + color: var(--accent-ink); + box-shadow: 0 2px 12px var(--accent-soft-strong); +} + +.btn--primary:hover { + box-shadow: 0 4px 18px var(--accent-soft-strong); +} + +.btn--ghost { + background: transparent; + border-color: var(--line); + color: var(--text); +} + +.btn--ghost:hover { + border-color: var(--line-strong); + background: var(--accent-soft); +} + +.btn--glass { + background: var(--bg-elev); + border-color: var(--line); + color: var(--text); + box-shadow: inset 0 1px 0 var(--line); +} + +.btn--glass:hover { + border-color: var(--line-strong); +} + +.btn--sm { + padding: 7px 14px; + font-size: 13.5px; +} + +.btn--lg { + padding: 13px 24px; + font-size: 16px; +} + +.btn--block { + width: 100%; +} + +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border: 1px solid var(--line); + border-radius: var(--radius-control); + background: transparent; + color: var(--text-dim); + cursor: pointer; + transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), + background-color 0.2s var(--ease-fast), transform 0.15s var(--ease-fast); +} + +.icon-btn:hover { + color: var(--text); + border-color: var(--line-strong); + background: var(--accent-soft); +} + +.icon-btn:active { + transform: scale(0.94); +} + +.icon-btn .icon { + width: 18px; + height: 18px; +} + +.icon--moon { + display: none; +} +html[data-theme="dark"] .icon-btn .icon--sun { + display: none; +} +html[data-theme="dark"] .icon-btn .icon--moon { + display: block; +} + +.icon { + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.link-btn { + background: none; + border: none; + padding: 0; + color: var(--accent); + font-size: 13.5px; + font-weight: 600; + text-decoration: none; + cursor: pointer; +} +.link-btn:hover { + text-decoration: underline; +} + +/* ---------------- Pills / chips ---------------- */ + +.pill, +.chip { + display: inline-flex; + align-items: center; + border-radius: var(--radius-pill); + font-family: var(--font-mono); + font-size: 12px; + white-space: nowrap; +} + +.pill { + padding: 5px 12px; + border: 1px solid var(--line); + color: var(--text-dim); +} + +.chip { + padding: 5px 12px; + border: 1px solid var(--line); + color: var(--text-dim); + background: var(--bg-elev); +} + +/* ---------------- Hero ---------------- */ + +.hero { + padding: 56px 0 72px; + position: relative; +} + +.hero__grid { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); + gap: 64px; + align-items: center; +} + +.hero__eyebrow { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 22px; +} + +.hero h1 { + font-size: clamp(2.5rem, 5.2vw, 3.7rem); + line-height: 1.04; + letter-spacing: -0.035em; + font-weight: 750; + margin-bottom: 20px; + max-width: 13ch; +} + +.hero__lede { + color: var(--text-dim); + font-size: 17px; + line-height: 1.55; + max-width: 52ch; + margin-bottom: 30px; +} + +.hero__cta { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 18px; +} + +.hero__hint { + color: var(--text-faint); + font-size: 13.5px; +} + +.hero__visual { + display: flex; + flex-direction: column; + gap: 14px; +} + +/* Terminal */ + +.terminal { + border: 1px solid var(--term-line); + border-radius: var(--radius-panel); + background: var(--term-bg); + box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.05); + overflow: hidden; +} + +.terminal__bar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid var(--term-line); + background: rgba(255, 255, 255, 0.02); +} + +.terminal__dot { + width: 11px; + height: 11px; + border-radius: 50%; +} +.terminal__dot--r { background: #e06c5b; } +.terminal__dot--y { background: #d9a35e; } +.terminal__dot--g { background: #5fbf8a; } + +.terminal__title { + margin-left: 6px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--term-dim); +} + +.terminal__body { + padding: 20px 22px 22px; + font-family: var(--font-mono); + font-size: 13.5px; + line-height: 1.75; + color: var(--term-text); +} + +.term-line { + display: flex; + align-items: baseline; + gap: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.term-prompt { + color: var(--term-accent); + font-weight: 700; + flex: none; +} + +.term-cmd { + white-space: pre-wrap; + word-break: break-all; + min-height: 1.6em; +} + +.term-copy { + margin-left: auto; + flex: none; + padding: 3px 10px; + border: 1px solid var(--term-line); + border-radius: 7px; + background: rgba(255, 255, 255, 0.03); + color: var(--term-dim); + font-family: var(--font-mono); + font-size: 11.5px; + cursor: pointer; + opacity: 0; + transform: translateY(2px); + transition: opacity 0.2s var(--ease-fast), transform 0.2s var(--ease-fast), + color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast); +} + +.term-line:hover .term-copy, +.term-copy:focus-visible, +.term-copy.is-visible { + opacity: 1; + transform: none; +} + +.term-copy:hover { + color: var(--term-text); + border-color: var(--term-line); +} + +.term-copy.is-copied { + color: var(--term-ok); + border-color: var(--term-ok); +} + +.term-copy.is-failed { + color: var(--danger); + border-color: var(--danger); +} + +.term-out { + color: var(--term-dim); +} + +.term-out .term-ok { + color: var(--term-accent); +} + +.term-out .term-check { + color: var(--term-ok); +} + +.term-cursor { + display: inline-block; + width: 8px; + height: 16px; + margin-left: 2px; + background: var(--term-accent); + vertical-align: text-bottom; +} + +.term-cursor-wrap { + min-height: 1.6em; +} + +@keyframes blink { + 0%, 55% { opacity: 1; } + 56%, 100% { opacity: 0; } +} + +.term-cursor { + animation: blink 1.1s steps(1) infinite; +} + +/* Release chip */ + +.release-chip { + display: inline-flex; + align-items: center; + gap: 12px; + align-self: flex-start; + padding: 8px 14px 8px 8px; + border: 1px solid var(--line); + border-radius: var(--radius-pill); + background: var(--bg-elev); + font-size: 13px; +} + +.release-chip__tag { + padding: 3px 11px; + border-radius: var(--radius-pill); + background: var(--accent-soft); + color: var(--accent); + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; +} + +.release-chip__meta { + color: var(--text-dim); +} + +.release-chip__meta a { + color: var(--text); + text-decoration: none; + border-bottom: 1px solid var(--line-strong); +} +.release-chip__meta a:hover { + color: var(--accent); + border-color: var(--accent); +} + +/* Spec strip */ + +.spec-strip { + padding: 0 24px; +} + +.spec-strip dl { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.spec-strip dl > div { + padding: 18px 20px; + border-left: 1px solid var(--line); +} + +.spec-strip dl > div:first-child { + border-left: 0; +} + +.spec-strip dt { + font-family: var(--font-mono); + font-size: 11.5px; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--text-faint); + margin-bottom: 6px; +} + +.spec-strip dd { + font-size: 13.5px; + font-weight: 600; + color: var(--text); + line-height: 1.45; +} + +/* ---------------- Sections ---------------- */ + +.section { + padding: 104px 0; +} + +.section--install { + background: linear-gradient(var(--bg-elev), var(--bg)); + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.section--checklist { + background: var(--bg-elev); + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.section__head { + margin-bottom: 56px; +} + +.section__head--row { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; + flex-wrap: wrap; +} + +.section__head h2 { + font-size: clamp(1.9rem, 3.4vw, 2.6rem); + letter-spacing: -0.025em; + line-height: 1.12; + font-weight: 750; + margin-bottom: 14px; +} + +.section__head .section__sub { + color: var(--text-dim); + max-width: 58ch; + font-size: 16px; +} + +.platform-chip { + flex: none; + padding: 7px 14px; + border: 1px solid var(--line); + border-radius: var(--radius-pill); + font-size: 13px; + color: var(--text-dim); +} + +.platform-chip .ok { + color: var(--ok); + font-weight: 600; +} + +.platform-chip .warn { + color: var(--warn); + font-weight: 600; +} + +/* ---------------- Spec grid ---------------- */ + +.spec-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 56px; +} + +.spec { + padding: 28px 0 36px; + border-top: 1px solid var(--line); +} + +.spec__name { + display: block; + font-size: 11.5px; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--text-faint); + margin-bottom: 10px; +} + +.spec__value { + font-size: 1.35rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 10px; +} + +.spec__body { + color: var(--text-dim); + font-size: 14.5px; + line-height: 1.6; + max-width: 46ch; +} + +.spec__body code, +.step__note code, +.expect code, +.panel__head code, +.faq code, +.step-panel code { + font-family: var(--font-mono); + font-size: 0.9em; + background: var(--accent-soft); + color: var(--text); + padding: 1px 6px; + border-radius: 5px; +} + +/* ---------------- Installer ---------------- */ + +.installer { + display: grid; + grid-template-columns: 250px minmax(0, 1fr); + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg); + overflow: hidden; + box-shadow: var(--shadow); +} + +.installer__rail { + display: flex; + flex-direction: column; + border-right: 1px solid var(--line); + background: var(--bg-elev); + padding: 10px; + gap: 4px; +} + +.method { + display: grid; + grid-template-columns: auto 1fr; + align-items: baseline; + gap: 10px; + padding: 14px 14px; + border: 1px solid transparent; + border-radius: var(--radius-control); + background: none; + color: var(--text-dim); + text-align: left; + cursor: pointer; + font-family: var(--font-ui); + transition: background-color 0.2s var(--ease-fast), color 0.2s var(--ease-fast), + border-color 0.2s var(--ease-fast); +} + +.method:hover { + color: var(--text); + background: var(--accent-soft); +} + +.method.is-active { + color: var(--text); + background: var(--accent-soft); + border-color: var(--accent-soft-strong); +} + +.method__num { + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--text-faint); +} + +.method.is-active .method__num { + color: var(--accent); +} + +.method__label { + font-weight: 600; + font-size: 14.5px; +} + +.method__tag { + grid-column: 2; + font-size: 11px; + font-family: var(--font-mono); + color: var(--accent); + background: var(--accent-soft); + border-radius: var(--radius-pill); + padding: 2px 9px; + width: fit-content; +} + +.installer__panel { + padding: 34px 36px 40px; +} + +.panel__head { + margin-bottom: 30px; +} + +.panel__head h3 { + font-size: 1.3rem; + letter-spacing: -0.02em; + margin-bottom: 8px; +} + +.panel__head p { + color: var(--text-dim); + font-size: 14.5px; + max-width: 60ch; +} + +/* Steps inside panels */ + +.panel .steps { + list-style: none; +} + +.panel .step { + display: grid; + grid-template-columns: 34px minmax(0, 1fr); + gap: 14px; + padding: 22px 0; + border-top: 1px solid var(--line); +} + +.panel .step:first-of-type { + padding-top: 6px; + border-top: 0; +} + +.step__num { + font-family: var(--font-mono); + font-size: 13px; + color: var(--accent); + font-weight: 700; + padding-top: 3px; +} + +.step__content h4 { + font-size: 15px; + margin-bottom: 10px; + letter-spacing: -0.01em; +} + +.step__note { + color: var(--text-dim); + font-size: 14px; + margin-bottom: 10px; + max-width: 62ch; +} + +/* Command blocks */ + +.cmd { + display: flex; + align-items: stretch; + border: 1px solid var(--line); + border-radius: var(--radius-control); + background: var(--bg-elev-2); + overflow: hidden; + max-width: 100%; +} + +.cmd pre { + flex: 1; + padding: 12px 16px; + overflow-x: auto; + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.6; + color: var(--term-text); + scrollbar-width: thin; +} + +.cmd__copy { + flex: none; + padding: 0 16px; + border: 0; + border-left: 1px solid var(--line); + background: transparent; + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: color 0.2s var(--ease-fast), background-color 0.2s var(--ease-fast); +} + +.cmd__copy:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.06); +} + +.cmd__copy.is-copied { + color: var(--term-ok); + background: rgba(95, 191, 138, 0.12); +} + +.cmd__copy.is-failed { + color: var(--danger); + background: rgba(209, 107, 107, 0.12); +} + +.cmd__copy--inline { + display: inline-block; + padding: 2px 8px; + border: 1px solid var(--line); + border-radius: 6px; + margin-left: 4px; + font-size: 11.5px; +} + +.expect { + color: var(--text-dim); + font-size: 13.5px; + margin-top: 12px; +} + +.expect + .cmd { + margin-top: 10px; +} + +/* Notice */ + +.notice { + margin-top: 32px; + border: 1px solid var(--line); + border-left: 3px solid var(--warn); + border-radius: var(--radius-control); + background: var(--bg-elev); + padding: 0 20px; +} + +.notice summary { + cursor: pointer; + padding: 14px 0; + font-weight: 600; + font-size: 14px; + color: var(--warn); + list-style: none; +} + +.notice summary::-webkit-details-marker { + display: none; +} + +.notice summary::after { + content: "+"; + float: right; + font-family: var(--font-mono); + color: var(--text-faint); +} + +.notice[open] summary::after { + content: "\2212"; +} + +.notice p { + padding: 0 0 16px; + color: var(--text-dim); + font-size: 14px; + max-width: 70ch; +} + +.notice a { + color: var(--accent); + text-decoration: none; + border-bottom: 1px solid var(--accent); +} +.notice a:hover { + text-decoration: underline; +} + +/* ---------------- Pipeline stepper ---------------- */ + +.pipeline { + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + gap: 24px; + align-items: start; +} + +.stepper { + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; +} + +.stepper__node { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 10px 12px; + border: 1px solid transparent; + border-radius: var(--radius-control); + background: none; + color: var(--text-dim); + font-family: var(--font-ui); + font-size: 14px; + font-weight: 500; + cursor: pointer; + text-align: left; + transition: background-color 0.2s var(--ease-fast), color 0.2s var(--ease-fast); +} + +.stepper__node:hover { + color: var(--text); + background: var(--accent-soft); +} + +.stepper__node span { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + flex: none; + border-radius: var(--radius-pill); + border: 1px solid var(--line); + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-faint); + transition: background-color 0.2s var(--ease-fast), color 0.2s var(--ease-fast), + border-color 0.2s var(--ease-fast); +} + +.stepper__step.is-active .stepper__node { + color: var(--text); + background: var(--accent-soft); +} + +.stepper__step.is-active .stepper__node span { + background: var(--accent); + border-color: var(--accent); + color: var(--accent-ink); + font-weight: 700; +} + +.stepper__step.is-done .stepper__node { + color: var(--text-dim); +} + +.stepper__step.is-done .stepper__node span { + background: var(--accent-soft); + border-color: var(--accent-soft-strong); + color: var(--accent); +} + +.step-stage { + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-elev); + padding: 34px 36px; + box-shadow: var(--shadow); +} + +.step-panel h3 { + font-size: 1.35rem; + letter-spacing: -0.02em; + margin-bottom: 12px; +} + +.step-panel p { + color: var(--text-dim); + font-size: 15px; + max-width: 58ch; + margin-bottom: 18px; +} + +.chips { + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.step-controls { + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid var(--line); + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.step-controls__progress { + height: 3px; + flex: 1; + min-width: 120px; + border-radius: 3px; + background: var(--line); + overflow: hidden; +} + +.step-controls__progress span { + display: block; + height: 100%; + width: 0; + background: var(--accent); + transition: width 0.4s var(--ease); +} + +.step-controls__buttons { + display: flex; + gap: 8px; +} + +.step-controls__status { + font-family: var(--font-mono); + font-size: 12.5px; + color: var(--text-faint); +} + +.step-controls .btn:disabled { + opacity: 0.4; + cursor: default; + transform: none; +} + +/* ---------------- Checklist ---------------- */ + +.checklist { + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg); + padding: 34px 36px 28px; + box-shadow: var(--shadow); +} + +.checklist__progress { + height: 4px; + border-radius: 4px; + background: var(--line); + overflow: hidden; + margin-bottom: 10px; +} + +.checklist__progress span { + display: block; + height: 100%; + width: 0; + background: var(--accent); + transition: width 0.45s var(--ease); +} + +.checklist__count { + font-family: var(--font-mono); + font-size: 12.5px; + color: var(--text-faint); + margin-bottom: 10px; +} + +.checklist__list { + list-style: none; +} + +.check { + display: grid; + grid-template-columns: 26px minmax(0, 1fr); + gap: 14px; + padding: 18px 0; + border-top: 1px solid var(--line); +} + +.check:first-child { + border-top: 0; +} + +.check input[type="checkbox"] { + appearance: none; + width: 22px; + height: 22px; + margin-top: 2px; + border: 1.5px solid var(--line-strong); + border-radius: 7px; + background: var(--bg-elev); + cursor: pointer; + position: relative; + transition: background-color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast); +} + +.check input[type="checkbox"]:hover { + border-color: var(--accent); +} + +.check input[type="checkbox"]:checked { + background: var(--accent); + border-color: var(--accent); +} + +.check input[type="checkbox"]:checked::after { + content: ""; + position: absolute; + left: 7px; + top: 3px; + width: 6px; + height: 11px; + border: solid var(--accent-ink); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.check label { + color: var(--text-dim); + font-size: 14.5px; + line-height: 1.6; + cursor: pointer; +} + +.check label strong { + color: var(--text); + font-weight: 600; +} + +.check.is-done label { + color: var(--text-faint); +} + +.checklist__done { + margin-top: 18px; + padding-top: 16px; + border-top: 1px solid var(--line); + color: var(--ok); + font-size: 14.5px; + font-weight: 600; +} + +.checklist .link-btn { + margin-top: 14px; +} + +/* ---------------- FAQ ---------------- */ + +.faq__item { + border-top: 1px solid var(--line); +} + +.faq__item:last-child { + border-bottom: 1px solid var(--line); +} + +.faq__item summary { + cursor: pointer; + padding: 20px 44px 20px 4px; + position: relative; + font-weight: 600; + font-size: 15.5px; + letter-spacing: -0.01em; + list-style: none; + color: var(--text); + transition: color 0.2s var(--ease-fast); +} + +.faq__item summary::-webkit-details-marker { + display: none; +} + +.faq__item summary:hover { + color: var(--accent); +} + +.faq__item summary::before, +.faq__item summary::after { + content: ""; + position: absolute; + right: 10px; + top: 50%; + width: 14px; + height: 1.5px; + background: var(--text-faint); + transition: transform 0.25s var(--ease), background-color 0.2s var(--ease-fast); +} + +.faq__item summary::after { + transform: rotate(90deg); +} + +.faq__item[open] summary::after { + transform: rotate(0deg); +} + +.faq__item[open] summary::before { + background: var(--accent); +} + +.faq__item p { + padding: 0 44px 22px 4px; + color: var(--text-dim); + font-size: 14.5px; + max-width: 68ch; +} + +/* ---------------- CTA ---------------- */ + +.cta { + padding: 112px 0; + text-align: center; + background: var(--bg-elev); + border-top: 1px solid var(--line); +} + +.cta h2 { + font-size: clamp(2.2rem, 4.5vw, 3.2rem); + letter-spacing: -0.03em; + line-height: 1.05; + font-weight: 750; + margin-bottom: 16px; +} + +.cta p { + color: var(--text-dim); + font-size: 17px; + max-width: 46ch; + margin: 0 auto 34px; +} + +.cta__actions { + display: flex; + justify-content: center; + gap: 12px; + flex-wrap: wrap; +} + +/* ---------------- Footer ---------------- */ + +.site-footer { + padding: 64px 0 32px; + border-top: 1px solid var(--line); +} + +.footer-grid { + display: grid; + grid-template-columns: 1.6fr 1fr 1fr 1fr; + gap: 40px; + padding-bottom: 48px; +} + +.footer-brand p { + color: var(--text-dim); + font-size: 14px; + margin-top: 14px; + max-width: 34ch; +} + +.footer-brand a { + color: var(--text-dim); +} + +.site-footer h4 { + font-size: 12.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-faint); + margin-bottom: 16px; +} + +.site-footer nav ul { + list-style: none; +} + +.site-footer nav li { + margin-bottom: 10px; +} + +.site-footer nav a { + color: var(--text-dim); + text-decoration: none; + font-size: 14px; + transition: color 0.15s var(--ease-fast); +} + +.site-footer nav a:hover { + color: var(--text); +} + +.footer-bottom { + padding-top: 24px; + border-top: 1px solid var(--line); +} + +.footer-bottom p { + color: var(--text-faint); + font-size: 12.5px; +} + +/* ---------------- Scroll reveal ---------------- */ + +/* Hidden initial state is gated on html.js (set by the inline head script), so + content stays visible when JS is disabled or blocked. */ +html.js .reveal { + opacity: 0; + transform: translateY(16px); + transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); +} + +html.js .reveal.in { + opacity: 1; + transform: none; +} + +/* ---------------- Responsive ---------------- */ + +@media (max-width: 960px) { + .hero__grid { + grid-template-columns: 1fr; + gap: 44px; + } + + .hero { + padding: 44px 0 56px; + } + + .spec-strip dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .spec-strip dl > div:nth-child(3) { + border-left: 0; + } + + .spec-strip dl > div:nth-child(5) { + border-left: 0; + grid-column: 1 / -1; + } + + .installer { + grid-template-columns: 1fr; + } + + .installer__rail { + flex-direction: row; + border-right: 0; + border-bottom: 1px solid var(--line); + overflow-x: auto; + padding: 8px; + } + + .method { + grid-template-columns: auto 1fr; + padding: 10px 14px; + min-width: max-content; + } + + .method__tag { + display: none; + } + + .pipeline { + grid-template-columns: 1fr; + } + + .stepper { + flex-direction: row; + overflow-x: auto; + padding-bottom: 4px; + } + + .stepper__node { + width: auto; + min-width: max-content; + } + + .footer-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 720px) { + .section { + padding: 72px 0; + } + + .nav__toggle { + display: flex; + } + + .nav__links { + display: none; + position: absolute; + top: 60px; + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + gap: 2px; + padding: 12px 16px 18px; + background: var(--header-bg); + -webkit-backdrop-filter: blur(18px) saturate(1.4); + backdrop-filter: blur(18px) saturate(1.4); + border-bottom: 1px solid var(--line); + } + + .nav__links.open { + display: flex; + } + + .nav__actions { + margin-left: 0; + padding-top: 8px; + border-top: 1px solid var(--line); + margin-top: 6px; + } + + .hero h1 { + max-width: none; + } + + .spec-grid { + grid-template-columns: 1fr; + gap: 0; + } + + .installer__panel { + padding: 24px 20px 28px; + } + + .step-stage { + padding: 24px 22px; + } + + .checklist { + padding: 26px 22px 22px; + } + + .footer-grid { + grid-template-columns: 1fr; + gap: 32px; + } + + .section__head--row { + align-items: flex-start; + } +} + +/* ---------------- Reduced motion ---------------- */ + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + html.js .reveal { + opacity: 1; + transform: none; + transition: none; + } + + .term-cursor { + animation: none; + } + + .btn, + .icon-btn, + .cmd__copy, + .term-copy { + transition: none; + } +}