diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..343317b --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,44 @@ +name: Deploy GitHub Pages + +on: + push: + branches: [main] + paths: + - "site/**" + - "CHANGELOG.md" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + name: deploy site/ + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v5 + + - name: Sync CHANGELOG.md into site/ + run: python3 site/sync_changelog.py + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload site artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 72f4692..7af81e3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ sdkconfig sdkconfig.old sdkconfig.ci.* +# Site: generated from root CHANGELOG.md (python3 site/sync_changelog.py) +site/changelog.data.js + # Host unit-test binaries tests/host/build/ diff --git a/brand/FlinT_logo.png b/brand/FlinT_logo.png index 5831ae4..116ce2a 100644 Binary files a/brand/FlinT_logo.png and b/brand/FlinT_logo.png differ diff --git a/site/.nojekyll b/site/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..a92c3e4 --- /dev/null +++ b/site/README.md @@ -0,0 +1,14 @@ +# Product landing (GitHub Pages) + +Static site for FlinT OS. Deployed by `.github/workflows/pages.yml`. + +## Local preview + +```bash +python3 site/sync_changelog.py # embed root CHANGELOG.md → changelog.data.js +# open site/index.html / site/changelog.html +``` + +Source of truth is the repo-root `CHANGELOG.md`. Pages only uploads `site/`, +so CI (and local sync) embed it into `changelog.data.js` — no duplicate +`site/CHANGELOG.md`, works under `file://`. diff --git a/site/assets/FlinT_logo.png b/site/assets/FlinT_logo.png new file mode 100644 index 0000000..8d1c619 Binary files /dev/null and b/site/assets/FlinT_logo.png differ diff --git a/site/assets/FlinT_logo.webp b/site/assets/FlinT_logo.webp new file mode 100644 index 0000000..e82a6cc Binary files /dev/null and b/site/assets/FlinT_logo.webp differ diff --git a/site/changelog.html b/site/changelog.html new file mode 100644 index 0000000..c719308 --- /dev/null +++ b/site/changelog.html @@ -0,0 +1,91 @@ + + + + + + Changelog — FlinT OS + + + + + + + + + + + + + + + +
+
+

Release history

+

Changelog

+

+ Sourced from CHANGELOG.md in the FlinT OS repository + (Keep a Changelog · SemVer). +

+
+
+ +
+

+ Preparing changelog… +

+ +
+ + + + + + + + diff --git a/site/changelog.js b/site/changelog.js new file mode 100644 index 0000000..76ea4b5 --- /dev/null +++ b/site/changelog.js @@ -0,0 +1,180 @@ +(() => { + const statusEl = document.getElementById("changelog-status"); + const rootEl = document.getElementById("changelog-root"); + if (!statusEl || !rootEl) return; + + const esc = (s) => + s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + + const inline = (text) => { + let t = esc(text); + t = t.replace( + /`([^`]+)`/g, + '$1' + ); + t = t.replace(/\*\*([^*]+)\*\*/g, "$1"); + t = t.replace( + /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, + '$1' + ); + return t; + }; + + const parseChangelog = (md) => { + const lines = md.replace(/\r\n/g, "\n").split("\n"); + const releases = []; + const links = {}; + let release = null; + let section = null; + let intro = []; + let inIntro = true; + let para = []; + + const flushPara = () => { + if (!para.length || !release || section) { + para = []; + return; + } + release.notes.push(para.join(" ")); + para = []; + }; + + for (const raw of lines) { + const line = raw.trimEnd(); + const linkDef = line.match(/^\[([^\]]+)\]:\s+(\S+)\s*$/); + if (linkDef) { + links[linkDef[1]] = linkDef[2]; + continue; + } + + const h2 = line.match(/^##\s+\[([^\]]+)\](?:\s+-\s+(.+))?\s*$/); + if (h2) { + flushPara(); + inIntro = false; + release = { + id: h2[1], + date: (h2[2] || "").trim(), + notes: [], + sections: [], + }; + section = null; + releases.push(release); + continue; + } + + if (inIntro) { + if (line.startsWith("# ")) continue; + if (!line.trim()) continue; + intro.push(line.trim()); + continue; + } + + if (!release) continue; + + const h3 = line.match(/^###\s+(.+)\s*$/); + if (h3) { + flushPara(); + section = { title: h3[1].trim(), items: [] }; + release.sections.push(section); + continue; + } + + const bullet = line.match(/^-\s+(.+)$/); + if (bullet) { + flushPara(); + if (!section) { + section = { title: "Notes", items: [] }; + release.sections.push(section); + } + section.items.push(bullet[1].trim()); + continue; + } + + const cont = line.match(/^\s{2,}(.+)$/); + if (cont && section && section.items.length) { + section.items[section.items.length - 1] += " " + cont[1].trim(); + continue; + } + + if (!line.trim()) { + flushPara(); + continue; + } + + if (!section) para.push(line.trim()); + } + flushPara(); + + for (const r of releases) { + r.href = links[r.id] || null; + } + return { intro, releases }; + }; + + const render = (data) => { + const parts = []; + if (data.intro.length) { + parts.push( + `

${data.intro.map(inline).join(" ")}

` + ); + } + + parts.push('
    '); + data.releases.forEach((rel, idx) => { + const ver = rel.href + ? `${esc(rel.id)}` + : `${esc(rel.id)}`; + const date = rel.date + ? `` + : ""; + + parts.push( + `
  1. ` + + `
    ${ver}${date}
    ` + ); + + for (const note of rel.notes) { + parts.push(`

    ${inline(note)}

    `); + } + + for (const sec of rel.sections) { + const kind = sec.title.toLowerCase().replace(/[^a-z]+/g, "-"); + parts.push( + `
    ` + + `

    ${esc(sec.title)}

      ` + ); + for (const item of sec.items) { + parts.push(`
    • ${inline(item)}
    • `); + } + parts.push("
    "); + } + parts.push("
  2. "); + }); + parts.push("
"); + + rootEl.innerHTML = parts.join(""); + rootEl.hidden = false; + statusEl.hidden = true; + + // Re-run reveal for injected nodes + document.dispatchEvent(new CustomEvent("flint:content")); + }; + + const fail = (msg) => { + statusEl.textContent = msg; + statusEl.classList.add("is-error"); + }; + + const md = window.__FLINT_CHANGELOG_MD__; + if (typeof md === "string" && md.trim()) { + render(parseChangelog(md)); + } else { + fail( + "Changelog data missing. Run: python3 site/sync_changelog.py" + ); + } +})(); diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..f860cd2 --- /dev/null +++ b/site/index.html @@ -0,0 +1,461 @@ + + + + + + FlinT OS + + + + + + + + + + + + + + + + +
+
+
+ + + + + FlinT + +
+ +
+

DeeTech Labs

+

FlinT OS

+

+ Open firmware for FlinT devices · pluggable faces · ESP32‑C3 / + ESP‑IDF
+ by DeeTech Labs · base board FlinT Spark +

+ +
+
+
+ +
+
+
+
+

What it is

+

FlinT OS for FlinT devices

+

+ Core services stay stable — time, Wi‑Fi, settings, shell, assets. + Faces are plugins: ship a new look without rewriting networking or + the clock. +

+

+ Faces read FaceContext and paint. They do not call + Wi‑Fi, HTTP, NVS, or settings APIs. +

+
+ +
+
+
+ Face A + Face B + Face … +
+ +
+ FlinT OS core + time · net · settings · shell +
+ +
Display
+
+
+
+
+ +
+
+
+

Architecture

+

Layers

+

+ From docs/ARCHITECTURE.md: board JSON and hardware up + through services into FaceContext, then face paint and + display present. +

+
+ +
+
    +
  1. + hardware / board JSON +
  2. +
  3. + services — time · net · settings/cfg · day_mode +
  4. +
  5. + FaceContext ← uiBuildContext +
  6. +
  7. + Face::onTick / onForceRedraw +
  8. +
  9. + display — partial or full present +
  10. +
+
+
+
+ +
+
+
+

Paint path

+

Events → dirty → paint

+

+ flintEventEmit → dirty bitfield → + onTick / onForceRedraw +

+
+ +
+
    +
  1. event
  2. +
  3. uiDirtyBridge
  4. +
  5. + uiMarkDirty +
  6. +
  7. + uiFaceTick +
  8. +
+
    +
  • SECOND
  • +
  • MINUTE
  • +
  • WIFI
  • +
  • SETTINGS
  • +
  • DAY_MODE
  • +
  • FORCE
  • +
+
+
+
+ +
+
+
+

Boot path

+

From reset to face

+

+ App bring-up mounts services, then the shell loop drives dirty + paint. Splash first; faces after the core is ready. +

+
+
+
    +
  1. 01 board JSON / display HAL
  2. +
  3. 02 time · net · settings/cfg
  4. +
  5. 03 assets on storage
  6. +
  7. 04 splash · event bus
  8. +
  9. 05 active face · shell
  10. +
+
+
+
+ +
+
+
+

Tick scope

+

SECOND / MINUTE cadence

+

+ Schematic of the soft-clock event pulse that marks + UI_DIRTY_SECOND and UI_DIRTY_MINUTE — + not live device telemetry. +

+
+
+ +
+ SECOND + MINUTE +
+
+
+
+ +
+
+
+

Hardware

+

FlinT Spark

+

+ Board id flint_spark · ESP32‑C3 · display ST7789 + 240×280 SPI (UI landscape 280×240). Pins and panel come from board + JSON. +

+ +
+

SPI display (board JSON)

+
+
SCK
GPIO 6
+
MOSI
GPIO 7
+
CS
GPIO 10
+
DC
GPIO 9
+
RST
GPIO 8
+
VCC / LED
3.3V
+
GND
GND
+
+
+
+ +
+
+ + + + + --:-- + Waiting for time + ST7789 · 240×280 + ESP32‑C3 + USB Serial/JTAG + + + +
+
+
+
+ +
+
+
+

Persistence

+

Three stores

+

+ UI prefs are not stored as FlinT keys in NVS. + cfg survives asset reflash on storage. +

+
+ +
+
    +
  • + storage + LittleFS + core.flintpack +
  • +
  • + cfg + LittleFS + ui.cfg · time.cfg +
  • +
  • + nvs + ESP‑IDF + system / Wi‑Fi +
  • +
+
+
+
+ +
+
+
+

Built-in

+

What ships today

+
+
    +
  • Digital face · day modes · glances
  • +
  • Soft-clock + SNTP + timezone DB
  • +
  • Wi‑Fi STA · settings on cfg LittleFS
  • +
  • Face picker · event bus · boot splash
  • +
  • ESP‑IDF · LovyanGFX (slim) · dual OTA
  • +
  • Input: USB serial (buttons later)
  • +
  • License: Apache 2.0
  • +
+
+
+ +
+
+
+

Quick start

+

Build for FlinT Spark

+ +
+
+
+ bash +
+
cp components/flint/config/secrets.example.h \
+   components/flint/config/secrets.h
+idf.py set-target esp32c3
+idf.py build
+idf.py -p /dev/ttyACM0 flash monitor
+
+
+
+
+ + + + + + diff --git a/site/site.js b/site/site.js new file mode 100644 index 0000000..ff5bd4e --- /dev/null +++ b/site/site.js @@ -0,0 +1,364 @@ +(() => { + const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + const reveal = (el) => el.classList.add("is-in"); + + const observe = (selector, onEnter) => { + const nodes = [...document.querySelectorAll(selector)]; + if (!nodes.length) return; + + if (reduce || !("IntersectionObserver" in window)) { + nodes.forEach(onEnter); + return; + } + + const pending = new Set(nodes); + + const tryReveal = (el) => { + if (!pending.has(el)) return; + pending.delete(el); + onEnter(el); + io.unobserve(el); + }; + + const flushVisible = () => { + for (const el of [...pending]) { + const rect = el.getBoundingClientRect(); + if (rect.top < window.innerHeight * 0.95) tryReveal(el); + } + }; + + const io = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + tryReveal(entry.target); + } + }, + { threshold: 0.05, rootMargin: "40px 0px -2% 0px" } + ); + + nodes.forEach((el) => io.observe(el)); + flushVisible(); + window.addEventListener("scroll", flushVisible, { passive: true }); + window.addEventListener("resize", flushVisible, { passive: true }); + document.addEventListener("flint:content", () => { + document.querySelectorAll(selector).forEach((el) => { + if (el.classList.contains("is-in")) return; + pending.add(el); + io.observe(el); + }); + flushVisible(); + }); + }; + + observe("[data-animate], [data-reveal]", reveal); + + const startCycle = (root) => { + if (root.dataset.cycling === "1") return; + root.dataset.cycling = "1"; + const kind = root.getAttribute("data-cycle"); + const items = + kind === "step" + ? root.querySelectorAll(".flow__step") + : root.querySelectorAll(":scope > li"); + if (items.length < 2) return; + + let i = 0; + items[0].classList.add("is-active"); + window.setInterval(() => { + items[i].classList.remove("is-active"); + i = (i + 1) % items.length; + items[i].classList.add("is-active"); + }, 1400); + }; + + if (!reduce) { + observe("[data-cycle]", (el) => { + reveal(el.closest(".viz") || el); + startCycle(el); + }); + } + + // Bash syntax highlight + const highlightBash = (codeEl) => { + const esc = (s) => + s.replace(/&/g, "&").replace(//g, ">"); + + const paint = (token) => { + if (/^\\$/.test(token)) return `\\`; + if (/^--?[a-zA-Z0-9-]+$/.test(token)) + return `${esc(token)}`; + if ( + /^(?:cp|idf\.py|set-target|build|flash|monitor|esp32c3)$/.test(token) + ) + return `${esc(token)}`; + if (/^\/dev\//.test(token)) + return `${esc(token)}`; + if (/\.(?:h|c|cpp|py|md|json)$/.test(token)) + return `${esc(token)}`; + return esc(token); + }; + + codeEl.innerHTML = codeEl.textContent + .split("\n") + .map((line) => { + if (!line) return ""; + const lead = line.match(/^\s*/)?.[0] || ""; + const body = line.slice(lead.length); + const bits = body.match(/\\$|\/dev\/\S+|\S+/g) || []; + return esc(lead) + bits.map(paint).join(" "); + }) + .join("\n"); + }; + + document + .querySelectorAll("code.language-bash") + .forEach((el) => highlightBash(el)); + + // Scroll progress + const progress = document.querySelector(".scroll-progress > span"); + if (progress) { + const onScroll = () => { + const max = document.documentElement.scrollHeight - window.innerHeight; + const p = max > 0 ? window.scrollY / max : 0; + progress.style.transform = `scaleX(${Math.min(1, Math.max(0, p))})`; + document.body.classList.toggle("is-scrolled", window.scrollY > 24); + }; + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + } + + // Cursor glow (fine pointer only) + const glow = document.querySelector(".cursor-glow"); + if (glow && !reduce && window.matchMedia("(pointer: fine)").matches) { + let x = 0; + let y = 0; + let gx = 0; + let gy = 0; + window.addEventListener( + "pointermove", + (e) => { + x = e.clientX; + y = e.clientY; + glow.classList.add("is-on"); + }, + { passive: true } + ); + const tickGlow = () => { + gx += (x - gx) * 0.12; + gy += (y - gy) * 0.12; + glow.style.transform = `translate(${gx}px, ${gy}px)`; + requestAnimationFrame(tickGlow); + }; + requestAnimationFrame(tickGlow); + } + + // Soft pointer parallax on hero glow + const spark = document.querySelector(".atmosphere__spark"); + if (spark && !reduce && window.matchMedia("(pointer: fine)").matches) { + let mx = 0; + let my = 0; + let sx = 0; + let sy = 0; + + window.addEventListener( + "pointermove", + (e) => { + mx = (e.clientX / window.innerWidth - 0.5) * 28; + my = (e.clientY / window.innerHeight - 0.5) * 18; + }, + { passive: true } + ); + + const tickParallax = () => { + sx += (mx - sx) * 0.06; + sy += (my - sy) * 0.06; + spark.style.transform = `translate(${sx}px, ${sy}px)`; + requestAnimationFrame(tickParallax); + }; + requestAnimationFrame(tickParallax); + } + + // Magnetic buttons + if (!reduce && window.matchMedia("(pointer: fine)").matches) { + document.querySelectorAll(".btn").forEach((btn) => { + btn.addEventListener("pointermove", (e) => { + const r = btn.getBoundingClientRect(); + const dx = e.clientX - (r.left + r.width / 2); + const dy = e.clientY - (r.top + r.height / 2); + btn.style.transform = `translate(${dx * 0.12}px, ${dy * 0.16}px)`; + }); + btn.addEventListener("pointerleave", () => { + btn.style.transform = ""; + }); + }); + } + + // Tick scope schematic (not live device data) + const scope = document.getElementById("tick-scope"); + if (scope && !reduce) { + const ctx = scope.getContext("2d"); + const DPR = Math.min(window.devicePixelRatio || 1, 2); + let w = 0; + let h = 0; + let t0 = performance.now(); + let running = true; + + const resizeScope = () => { + const cssW = scope.clientWidth || 640; + const cssH = 180; + w = cssW; + h = cssH; + scope.width = Math.floor(cssW * DPR); + scope.height = Math.floor(cssH * DPR); + scope.style.height = `${cssH}px`; + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + }; + + const drawScope = (now) => { + if (!running) return; + const t = (now - t0) / 1000; + ctx.clearRect(0, 0, w, h); + + ctx.strokeStyle = "rgba(232,230,227,0.08)"; + ctx.lineWidth = 1; + for (let y = 20; y < h; y += 28) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(w, y); + ctx.stroke(); + } + + const mid = h * 0.55; + const drawPulseTrain = (period, color, amp, phase) => { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = 2; + let started = false; + for (let x = 0; x <= w; x++) { + const u = (x / w) * 8 + t / period + phase; + const beat = Math.abs(((u % 1) + 1) % 1); + const spike = beat < 0.08 ? Math.sin((beat / 0.08) * Math.PI) : 0; + const y = mid - spike * amp; + if (!started) { + ctx.moveTo(x, y); + started = true; + } else ctx.lineTo(x, y); + } + ctx.stroke(); + }; + + drawPulseTrain(1.0, "rgba(255,106,0,0.85)", h * 0.32, 0); + drawPulseTrain(6.0, "rgba(158,203,255,0.7)", h * 0.18, 0.15); + + // Playhead + const px = ((t * 0.12) % 1) * w; + ctx.strokeStyle = "rgba(255,255,255,0.2)"; + ctx.beginPath(); + ctx.moveTo(px, 12); + ctx.lineTo(px, h - 12); + ctx.stroke(); + + requestAnimationFrame(drawScope); + }; + + document.addEventListener("visibilitychange", () => { + running = document.visibilityState === "visible"; + if (running) requestAnimationFrame(drawScope); + }); + + window.addEventListener("resize", resizeScope, { passive: true }); + resizeScope(); + requestAnimationFrame(drawScope); + } else if (scope && reduce) { + const ctx = scope.getContext("2d"); + if (ctx) { + scope.width = 640; + scope.height = 180; + ctx.fillStyle = "rgba(255,106,0,0.15)"; + ctx.fillRect(0, 80, 640, 2); + } + } + + // Signal field + const canvas = document.getElementById("signal-field"); + if (!canvas || reduce) return; + + const ctx = canvas.getContext("2d", { alpha: true }); + if (!ctx) return; + + const DPR = Math.min(window.devicePixelRatio || 1, 2); + let fw = 0; + let fh = 0; + let nodes = []; + let running = true; + + const resize = () => { + fw = window.innerWidth; + fh = window.innerHeight; + canvas.width = Math.floor(fw * DPR); + canvas.height = Math.floor(fh * DPR); + canvas.style.width = `${fw}px`; + canvas.style.height = `${fh}px`; + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + const count = Math.round(Math.min(40, Math.max(16, (fw * fh) / 48000))); + nodes = Array.from({ length: count }, () => ({ + x: Math.random() * fw, + y: Math.random() * fh, + vx: (Math.random() - 0.5) * 0.22, + vy: (Math.random() - 0.5) * 0.22, + r: 1 + Math.random() * 1.3, + })); + }; + + const draw = () => { + if (!running) return; + ctx.clearRect(0, 0, fw, fh); + + for (const n of nodes) { + n.x += n.vx; + n.y += n.vy; + if (n.x < -20) n.x = fw + 20; + if (n.x > fw + 20) n.x = -20; + if (n.y < -20) n.y = fh + 20; + if (n.y > fh + 20) n.y = -20; + } + + const linkDist = Math.min(150, fw * 0.11); + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const a = nodes[i]; + const b = nodes[j]; + const d = Math.hypot(a.x - b.x, a.y - b.y); + if (d > linkDist) continue; + const alpha = (1 - d / linkDist) * 0.16; + ctx.strokeStyle = `rgba(255, 106, 0, ${alpha})`; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + } + } + + for (const n of nodes) { + ctx.fillStyle = "rgba(255, 106, 0, 0.4)"; + ctx.beginPath(); + ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); + ctx.fill(); + } + + requestAnimationFrame(draw); + }; + + document.addEventListener("visibilitychange", () => { + running = document.visibilityState === "visible"; + if (running) requestAnimationFrame(draw); + }); + + window.addEventListener("resize", resize, { passive: true }); + resize(); + requestAnimationFrame(draw); +})(); diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..862c5d9 --- /dev/null +++ b/site/styles.css @@ -0,0 +1,1704 @@ +:root { + --bg: #070708; + --ink: #e8e6e3; + --muted: #9a9690; + --line: rgba(232, 230, 227, 0.14); + --spark: #ff6a00; + --spark-soft: rgba(255, 106, 0, 0.35); + --font-display: "Sora", sans-serif; + --font-body: "Sora", sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; + --pad-x: clamp(1rem, 3.5vw, 2.75rem); + --shell: min(1180px, 100%); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + -webkit-text-size-adjust: 100%; +} + +html, +body { + margin: 0; + min-height: 100%; +} + +body { + color: var(--ink); + font-family: var(--font-body); + background: var(--bg); + min-height: 100dvh; + overflow-x: clip; +} + +a { + color: inherit; +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + color: #ffb27a; + word-break: break-word; + padding: 0.12em 0.32em; + border-radius: 4px; + background: rgba(255, 106, 0, 0.08); + line-height: 1.35; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} + +.shell { + width: var(--shell); + margin-inline: auto; + padding-inline: var(--pad-x); +} + +.atmosphere { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(ellipse 70% 50% at 18% -5%, rgba(255, 106, 0, 0.12), transparent 55%), + radial-gradient(ellipse 50% 40% at 92% 35%, rgba(255, 106, 0, 0.05), transparent 50%), + linear-gradient(180deg, #0c0c0e 0%, var(--bg) 42%, #050506 100%); +} + +.atmosphere__field { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0.55; +} + +.atmosphere__grid { + position: absolute; + inset: 0; + opacity: 0.18; + background-image: + linear-gradient(var(--line) 1px, transparent 1px), + linear-gradient(90deg, var(--line) 1px, transparent 1px); + background-size: 56px 56px; + mask-image: linear-gradient(180deg, #000 0%, transparent 85%); + animation: grid-drift 28s linear infinite; +} + +.atmosphere__spark { + position: absolute; + top: 8%; + left: 22%; + width: min(320px, 50vw); + height: min(320px, 50vw); + background: radial-gradient(circle, rgba(255, 106, 0, 0.16) 0%, transparent 70%); + animation: spark-breathe 5.5s ease-in-out infinite; + will-change: transform; +} + +/* —— Hero —— */ +.hero { + position: relative; + z-index: 1; + min-height: 100dvh; + min-height: 100svh; + display: flex; + align-items: center; + padding-block: clamp(4.5rem, 10vh, 6rem) clamp(2.5rem, 8vh, 5rem); +} + +.hero__inner { + display: grid; + gap: clamp(1.5rem, 4vw, 3.5rem); + align-items: center; + width: var(--shell); +} + +.hero__brand { + position: relative; + display: inline-grid; + place-items: center; + justify-self: start; + padding: 1.35rem; +} + +.hero__ring { + position: absolute; + inset: 0.35rem; + border: 1px dashed rgba(255, 106, 0, 0.32); + border-radius: 50%; + animation: ring-spin 22s linear infinite; + pointer-events: none; +} + +.hero__ring--delay { + inset: -0.15rem; + border-style: solid; + border-color: rgba(255, 106, 0, 0.1); + animation-duration: 32s; + animation-direction: reverse; +} + +.hero__picture { + position: relative; + z-index: 1; +} + +.hero__mark { + display: block; + width: min(240px, 56vw); + height: auto; + border-radius: 24px; + opacity: 0; + transform: translateY(12px); + animation: rise-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) 0.05s forwards; +} + +.hero__eyebrow { + margin: 0 0 0.55rem; + color: var(--spark); + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + opacity: 0; + transform: translateY(12px); + animation: rise-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) 0.1s forwards; +} + +.hero__title { + margin: 0 0 0.75rem; + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(2.4rem, 7vw, 4.25rem); + line-height: 1.1; + letter-spacing: -0.025em; + color: var(--ink); + opacity: 0; + transform: translateY(12px); + animation: rise-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) 0.18s forwards; + padding-block: 0.06em; +} + +.hero__title span { + color: #ffb27a; +} + +.hero__lede { + margin: 0; + max-width: 38rem; + color: var(--muted); + font-size: clamp(0.98rem, 2.1vw, 1.15rem); + line-height: 1.55; + opacity: 0; + transform: translateY(12px); + animation: rise-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) 0.3s forwards; +} + +.hero__cta { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.35rem; + opacity: 0; + transform: translateY(12px); + animation: rise-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) 0.42s forwards; +} + +.hero__cta--static { + opacity: 1; + transform: none; + animation: none; + margin-top: 1.25rem; +} + +@media (min-width: 860px) { + .hero__inner { + grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); + } + + .atmosphere__spark { + left: 12%; + top: 18%; + } +} + +/* —— Sections —— */ +main { + position: relative; + z-index: 1; +} + +.block { + padding-block: clamp(2.75rem, 7vw, 5rem); + border-top: 1px solid var(--line); +} + +.block__grid { + display: grid; + gap: clamp(1.75rem, 4vw, 3rem); + align-items: start; +} + +@media (min-width: 800px) { + .block__grid { + grid-template-columns: minmax(15rem, 0.95fr) minmax(0, 1.25fr); + } + + .block__grid--stack-first { + grid-template-columns: minmax(0, 1.2fr) minmax(15rem, 0.85fr); + } + + .block__grid--stack-first .block__copy { + order: 2; + } + + .block__grid--stack-first .viz { + order: 1; + } +} + +.block__copy--wide { + max-width: 48rem; + margin-bottom: 1.5rem; +} + +.block__copy[data-reveal] { + opacity: 0; + transform: translateY(16px); + transition: + opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.7s cubic-bezier(0.22, 1, 0.36, 1); +} + +.block__copy[data-reveal].is-in { + opacity: 1; + transform: none; +} + +.block__label { + margin: 0 0 0.7rem; + color: var(--spark); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + position: relative; + display: inline-block; +} + +.block__label::after { + content: ""; + display: block; + height: 1px; + width: 0; + margin-top: 0.45rem; + background: linear-gradient(90deg, var(--spark), transparent); + transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.15s; +} + +.block__copy.is-in .block__label::after { + width: 100%; +} + +.block__title { + margin: 0 0 0.9rem; + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(1.65rem, 3.4vw, 2.5rem); + line-height: 1.2; + letter-spacing: -0.03em; + overflow-wrap: anywhere; + padding-block: 0.04em; +} + +.block__body { + margin: 0; + color: var(--muted); + font-size: clamp(1rem, 1.5vw, 1.08rem); + line-height: 1.65; + max-width: 40rem; +} + +.block__note { + margin: 1.1rem 0 0; + padding-left: 0.85rem; + border-left: 2px solid rgba(255, 106, 0, 0.55); + color: var(--ink); + font-size: 0.95rem; + line-height: 1.55; + max-width: 36rem; +} + +/* —— Visualizations —— */ +.viz { + margin: 0; + padding: 0; + min-width: 0; + opacity: 0; + transform: translateY(14px); + transition: + opacity 0.65s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.65s cubic-bezier(0.22, 1, 0.36, 1); +} + +.viz.is-in { + opacity: 1; + transform: none; +} + +.viz__svg { + display: block; + width: 100%; + height: auto; + overflow: visible; +} + +.viz__svg--device { + max-width: 320px; + margin-inline: auto; +} + +.viz--device { + display: flex; + justify-content: center; +} + +.device-frame { + position: relative; + display: inline-block; + max-width: 320px; + width: 100%; +} + +.device-frame .viz__svg--device { + max-width: none; + margin: 0; +} + +.device-scan { + position: absolute; + left: 22.5%; + right: 22.5%; + top: 17%; + height: 12%; + border-radius: 2px; + background: linear-gradient( + 180deg, + transparent, + rgba(255, 106, 0, 0.14), + transparent + ); + opacity: 0; + pointer-events: none; + mix-blend-mode: screen; +} + +.viz.is-in .device-scan { + opacity: 1; + animation: scan-y 3.8s ease-in-out infinite; +} + +/* Layers (HTML, reflows) */ +.layers { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.55rem; +} + +.layers__item { + position: relative; + padding: 1rem 1.15rem; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(18, 18, 20, 0.85); + font-family: var(--font-mono); + font-size: clamp(0.78rem, 1.5vw, 0.92rem); + line-height: 1.45; + overflow-wrap: anywhere; + transition: + border-color 0.35s ease, + background 0.35s ease, + box-shadow 0.35s ease; +} + +.layers__item.is-active { + border-color: rgba(255, 106, 0, 0.65); + background: rgba(255, 106, 0, 0.1); + box-shadow: 0 0 0 1px rgba(255, 106, 0, 0.12); +} + +.layers__item--core { + border-color: rgba(255, 106, 0, 0.5); + background: rgba(255, 106, 0, 0.08); + color: #ffc299; +} + +.layers__item--out { + border-color: rgba(232, 230, 227, 0.28); +} + +.layers__item:not(:last-child)::after { + content: ""; + position: absolute; + left: 1.35rem; + bottom: -0.55rem; + width: 1px; + height: 0.55rem; + background: var(--spark-soft); +} + +/* Flow */ +.flow { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: 1fr; + gap: 0.55rem; +} + +@media (min-width: 720px) { + .flow { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + } +} + +.flow__step { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 3.4rem; + padding: 0.75rem 0.85rem; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(18, 18, 20, 0.9); + font-family: var(--font-mono); + font-size: clamp(0.78rem, 1.4vw, 0.9rem); + text-align: center; + transition: + border-color 0.35s ease, + background 0.35s ease, + box-shadow 0.35s ease, + transform 0.35s ease; +} + +.flow__step.is-active { + border-color: rgba(255, 106, 0, 0.7); + background: rgba(255, 106, 0, 0.12); + box-shadow: 0 0 24px rgba(255, 106, 0, 0.12); + transform: translateY(-2px); + color: #ffc299; +} + +.flow__step.is-active::after { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + pointer-events: none; + background: linear-gradient( + 110deg, + transparent 30%, + rgba(255, 106, 0, 0.18) 48%, + transparent 66% + ); + background-size: 220% 100%; + animation: sweep 1.3s ease; +} + +.flow__step--core { + border-color: rgba(255, 106, 0, 0.5); + background: rgba(255, 106, 0, 0.08); + color: #ffc299; +} + +.flow__step--out { + border-color: rgba(232, 230, 227, 0.28); +} + +.bits { + list-style: none; + margin: 1rem 0 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.bits li { + padding: 0.35rem 0.65rem; + border: 1px solid var(--line); + border-radius: 6px; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.04em; + color: var(--muted); +} + +.viz.is-in .bits li { + animation: bit-glow 3.6s ease-in-out infinite; + animation-delay: calc(var(--i, 0) * 0.4s); +} + +/* Stores */ +.stores { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.75rem; +} + +@media (min-width: 720px) { + .stores { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + } +} + +.stores__item { + display: grid; + gap: 0.35rem; + padding: 1.15rem 1.2rem; + border: 1px solid var(--line); + border-radius: 10px; + background: rgba(18, 18, 20, 0.9); + font-family: var(--font-mono); + font-size: 0.88rem; + color: var(--muted); + transition: + border-color 0.35s ease, + background 0.35s ease, + transform 0.35s ease, + box-shadow 0.35s ease; +} + +.stores__item.is-active { + border-color: rgba(255, 106, 0, 0.65); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.25); + transform: translateY(-3px); +} + +.stores__item strong { + color: var(--ink); + font-size: 1.05rem; + font-weight: 600; +} + +.stores__item--core { + border-color: rgba(255, 106, 0, 0.5); + background: rgba(255, 106, 0, 0.08); +} + +.stores__item--core strong { + color: #ffc299; +} + +/* Pinout */ +.pinout { + margin-top: 1.5rem; +} + +.pinout__title { + margin: 0 0 0.75rem; + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--spark); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.pinout__list { + margin: 0; + padding: 0; + display: grid; + gap: 0; +} + +@media (min-width: 520px) { + .pinout__list { + grid-template-columns: 1fr 1fr; + column-gap: 1.25rem; + } +} + +.pinout__list > div { + display: grid; + grid-template-columns: 5.2rem 1fr; + gap: 0.5rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--line); + font-family: var(--font-mono); + font-size: 0.84rem; +} + +.pinout__list dt { + margin: 0; + color: var(--muted); +} + +.pinout__list dd { + margin: 0; + color: var(--ink); +} + +/* Facts */ +.facts { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0; +} + +@media (min-width: 960px) { + .facts { + grid-template-columns: 1fr 1fr; + column-gap: 2rem; + } +} + +.facts li { + padding: 0.9rem 0; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 1.02rem; + line-height: 1.45; +} + +@media (min-width: 720px) { + .facts li:nth-child(2n) { + border-top: 1px solid var(--line); + } + + .facts li:nth-child(-n + 2) { + border-top: 1px solid var(--line); + } +} + +.facts li:last-child { + border-bottom: 1px solid var(--line); +} + +@media (min-width: 720px) { + .facts li:nth-last-child(2):nth-child(odd) { + border-bottom: 1px solid var(--line); + } +} + +/* Faces plug diagram (responsive HTML) */ +.plug { + display: grid; + gap: 0.75rem; + align-items: center; +} + +@media (min-width: 700px) { + .plug { + grid-template-columns: minmax(6.5rem, 0.7fr) auto minmax(0, 1.2fr) auto minmax(5.5rem, 0.7fr); + gap: 0.65rem; + } +} + +.plug__faces { + display: grid; + gap: 0.45rem; +} + +.plug__node, +.plug__core, +.plug__out { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.25rem; + min-height: 2.8rem; + padding: 0.8rem 0.9rem; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(18, 18, 20, 0.9); + font-family: var(--font-mono); + font-size: 0.84rem; + line-height: 1.35; + text-align: center; + overflow-wrap: anywhere; +} + +.plug__core { + border-color: rgba(255, 106, 0, 0.5); + background: rgba(255, 106, 0, 0.08); + min-height: 4.5rem; +} + +.plug__core strong { + color: #ffc299; + font-weight: 600; +} + +.plug__core span { + color: var(--muted); + font-size: 0.72rem; +} + +.plug__out { + border-color: rgba(232, 230, 227, 0.28); +} + +.plug__join { + justify-self: center; + width: 1.25rem; + height: 1px; + background: linear-gradient(90deg, transparent, var(--spark-soft), transparent); +} + +@media (max-width: 699px) { + .plug__join { + width: 1px; + height: 0.85rem; + background: linear-gradient(180deg, transparent, var(--spark-soft), transparent); + } +} + +.viz.is-in .plug__node, +.viz.is-in .plug__core, +.viz.is-in .plug__out { + animation: layer-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--i, 0) * 80ms + 60ms); +} + +/* Code panel + syntax */ +.codepanel { + min-width: 0; + border: 1px solid var(--line); + border-radius: 12px; + overflow: hidden; + background: #0b0b0e; +} + +.codepanel__bar { + display: flex; + align-items: center; + padding: 0.55rem 0.9rem; + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.02); +} + +.codepanel__lang { + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--spark); +} + +.codeblock { + margin: 0; + padding: 1.05rem 1.1rem 1.15rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + background: transparent; + border: 0; + border-radius: 0; + color: #d7d3cc; + font-family: var(--font-mono); + font-size: clamp(0.72rem, 2.4vw, 0.84rem); + line-height: 1.6; +} + +.codeblock code { + color: inherit; + font-size: inherit; + white-space: pre; +} + +.tok-cmd { + color: #ff8a3d; + font-weight: 500; +} + +.tok-flag { + color: #9ecbff; +} + +.tok-file { + color: #e6c07b; +} + +.tok-path { + color: #7ee0c1; +} + +.tok-str { + color: #c3e88d; +} + +.tok-punct { + color: #8b8690; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + min-height: 2.85rem; + padding: 0.7rem 1.2rem; + border-radius: 10px; + font-family: var(--font-body); + font-size: 0.95rem; + font-weight: 600; + text-decoration: none; + transition: + background 160ms ease, + color 160ms ease, + border-color 160ms ease, + transform 160ms ease; +} + +.btn__icon { + width: 1.05rem; + height: 1.05rem; + flex-shrink: 0; + fill: currentColor; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(0); +} + +.btn--primary { + background: var(--spark); + color: #140a00; + border: 1px solid var(--spark); +} + +.btn--primary:hover { + background: #ff7d1f; + border-color: #ff7d1f; +} + +.btn--ghost { + background: transparent; + color: var(--ink); + border: 1px solid var(--line); +} + +.btn--ghost:hover { + border-color: rgba(255, 106, 0, 0.55); + background: rgba(255, 106, 0, 0.08); +} + +/* Footer */ +.foot { + position: relative; + z-index: 1; + padding-block: 2rem; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 0.82rem; +} + +.foot__inner { + display: flex; + flex-wrap: wrap; + gap: 0.65rem 1.25rem; + justify-content: space-between; + align-items: center; +} + +.foot__brand, +.foot__meta { + margin: 0; +} + +.foot__meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.foot a { + color: var(--muted); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: color 160ms ease, border-color 160ms ease; +} + +.foot a:hover { + color: var(--spark); + border-bottom-color: rgba(255, 106, 0, 0.45); +} + +/* SVG schematic bits still used in faces diagram */ +.schem-node { + fill: rgba(18, 18, 20, 0.92); + stroke: var(--line); + stroke-width: 1.5; +} + +.schem-node--core { + stroke: rgba(255, 106, 0, 0.55); + fill: rgba(255, 106, 0, 0.08); +} + +.schem-node--out { + stroke: rgba(232, 230, 227, 0.28); +} + +.schem-label { + fill: var(--ink); + font-family: var(--font-mono); + font-size: 13px; + text-anchor: middle; + dominant-baseline: middle; +} + +.schem-label--strong { + fill: #ffc299; + font-weight: 500; +} + +.schem-sub { + fill: var(--muted); + font-family: var(--font-mono); + font-size: 11px; + text-anchor: middle; +} + +.schem-wire { + fill: none; + stroke: var(--spark-soft); + stroke-width: 1.5; + stroke-linecap: round; + stroke-dasharray: 6 6; +} + +.schem-pulse { + fill: var(--spark); + opacity: 0; +} + +.device-body { + fill: #101012; + stroke: rgba(232, 230, 227, 0.2); + stroke-width: 1.5; +} + +.device-bezel { + fill: #0a0a0c; + stroke: rgba(232, 230, 227, 0.1); + stroke-width: 1; +} + +.device-screen { + fill: #050506; + stroke: rgba(255, 106, 0, 0.25); + stroke-width: 1; +} + +.device-clock { + fill: var(--muted); + font-family: var(--font-display); + font-size: 28px; + font-weight: 700; + text-anchor: middle; + letter-spacing: 0.06em; +} + +.device-sub { + fill: rgba(154, 150, 144, 0.85); + font-family: var(--font-body); + font-size: 11px; + text-anchor: middle; +} + +.device-dot { + fill: var(--spark); +} + +/* Reveal children */ +.viz.is-in .schem-face, +.viz.is-in .layers__item, +.viz.is-in .flow__step, +.viz.is-in .stores__item { + animation: layer-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--i, 0) * 80ms + 60ms); +} + +.viz.is-in .schem-wire { + animation: dash-flow 1.4s linear infinite; + animation-delay: 0.2s; +} + +.viz.is-in .schem-pulse { + animation: pulse-run 2.4s ease-in-out infinite; + animation-delay: 0.35s; +} + +.viz.is-in .device-screen { + animation: screen-breathe 4s ease-in-out infinite; +} + +.viz.is-in .device-dot { + animation: dot-blink 2.2s ease-in-out infinite; +} + +.viz.is-in .pinout__list > div { + animation: layer-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes rise-in { + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes spark-breathe { + 0%, + 100% { + opacity: 0.5; + } + 50% { + opacity: 0.85; + } +} + +@keyframes grid-drift { + from { + background-position: 0 0; + } + to { + background-position: 56px 56px; + } +} + +@keyframes ring-spin { + to { + transform: rotate(360deg); + } +} + +@keyframes title-shine { + 0%, + 100% { + background-position: 100% 0; + } + 50% { + background-position: 0% 0; + } +} + +@keyframes sweep { + from { + background-position: 120% 0; + } + to { + background-position: -20% 0; + } +} + +@keyframes scan-y { + 0%, + 100% { + transform: translateY(0); + opacity: 0; + } + 12% { + opacity: 0.9; + } + 55% { + opacity: 0.55; + } + 85% { + transform: translateY(220%); + opacity: 0; + } +} + +@keyframes layer-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes dash-flow { + to { + stroke-dashoffset: -24; + } +} + +@keyframes pulse-run { + 0% { + opacity: 0; + transform: translateX(0); + } + 15% { + opacity: 1; + } + 85% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateX(28px); + } +} + +@keyframes bit-glow { + 0%, + 80%, + 100% { + opacity: 0.45; + color: var(--muted); + border-color: var(--line); + } + 40% { + opacity: 1; + color: var(--spark); + border-color: rgba(255, 106, 0, 0.45); + } +} + +@keyframes screen-breathe { + 0%, + 100% { + stroke: rgba(255, 106, 0, 0.2); + } + 50% { + stroke: rgba(255, 106, 0, 0.55); + } +} + +@keyframes dot-blink { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 1; + } +} + +/* —— Mobile —— */ +@media (max-width: 599px) { + .hero { + min-height: auto; + padding-block: 2.25rem 2.75rem; + align-items: flex-start; + } + + .hero__inner { + justify-items: stretch; + } + + .hero__brand { + justify-self: center; + padding: 0.5rem; + } + + .hero__copy { + text-align: center; + } + + .hero__lede { + margin-inline: auto; + } + + .hero__cta, + .hero__cta--static { + width: 100%; + justify-content: center; + } + + .hero__cta .btn, + .hero__cta--static .btn { + flex: 1 1 calc(50% - 0.4rem); + min-width: 8.5rem; + } + + .hero__ring, + .hero__ring--delay { + display: none; + } + + .atmosphere__field { + opacity: 0.28; + } + + .foot__inner { + flex-direction: column; + align-items: flex-start; + } + + .block__title { + max-width: none; + } + + .hero__cta--static { + justify-content: stretch; + } +} + +/* —— Chrome: nav / progress / cursor —— */ +.scroll-progress { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 40; + height: 2px; + background: transparent; + pointer-events: none; +} + +.scroll-progress > span { + display: block; + height: 100%; + width: 100%; + transform-origin: left center; + transform: scaleX(0); + background: linear-gradient(90deg, var(--spark), #ffb27a); +} + +.cursor-glow { + position: fixed; + top: 0; + left: 0; + z-index: 3; + width: 280px; + height: 280px; + margin: -140px 0 0 -140px; + border-radius: 50%; + pointer-events: none; + opacity: 0; + background: radial-gradient(circle, rgba(255, 106, 0, 0.12), transparent 68%); + transition: opacity 0.35s ease; + mix-blend-mode: screen; +} + +.cursor-glow.is-on { + opacity: 1; +} + +.topnav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 30; + padding-block: 0.85rem; + background: rgba(7, 7, 8, 0); + border-bottom: 1px solid transparent; + transition: + background 0.25s ease, + border-color 0.25s ease, + backdrop-filter 0.25s ease; +} + +body.is-scrolled .topnav { + background: rgba(7, 7, 8, 0.72); + border-bottom-color: var(--line); + backdrop-filter: blur(12px); +} + +.topnav__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.topnav__brand { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-family: var(--font-display); + font-weight: 700; + font-size: 1.05rem; + letter-spacing: -0.02em; + text-decoration: none; + color: var(--ink); +} + +.topnav__spark { + width: 1.05rem; + height: 1.05rem; + flex-shrink: 0; + color: var(--spark); +} + +.topnav__brand span { + color: #ffb27a; +} + +.topnav__links { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 1rem; + font-size: 0.88rem; +} + +.topnav__links a { + color: var(--muted); + text-decoration: none; + transition: color 0.2s ease; +} + +.topnav__links a:hover, +.topnav__links a[aria-current="page"] { + color: var(--ink); +} + +/* Boot sequence */ +.boot { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.55rem; + counter-reset: none; +} + +.boot > li { + display: grid; + grid-template-columns: 2.4rem 1fr; + gap: 0.75rem; + align-items: center; + padding: 0.85rem 1rem; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(18, 18, 20, 0.9); + font-family: var(--font-mono); + font-size: clamp(0.78rem, 1.5vw, 0.9rem); + color: var(--muted); + transition: + border-color 0.35s ease, + background 0.35s ease, + transform 0.35s ease, + color 0.35s ease; +} + +.boot > li > span { + color: var(--spark); + font-weight: 500; +} + +.boot > li.is-active { + border-color: rgba(255, 106, 0, 0.65); + background: rgba(255, 106, 0, 0.1); + color: var(--ink); +} + +.viz.is-in .boot > li { + animation: layer-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--i, 0) * 80ms + 60ms); +} + +/* Tick scope */ +.viz--scope { + display: grid; + gap: 0.75rem; +} + +.scope { + display: block; + width: 100%; + height: 180px; + border: 1px solid var(--line); + border-radius: 10px; + background: + linear-gradient(180deg, rgba(255, 106, 0, 0.04), transparent 40%), + #0b0b0e; +} + +.scope__legend { + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--muted); +} + +.scope__legend span { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.scope__swatch { + display: inline-block; + width: 0.7rem; + height: 0.7rem; + border-radius: 2px; +} + +.scope__swatch--sec { + background: var(--spark); +} + +.scope__swatch--min { + background: #9ecbff; +} + +/* Changelog page */ +.page-hero { + position: relative; + z-index: 1; + padding-block: 6.5rem 2.5rem; +} + +.page-hero__title { + margin: 0 0 0.75rem; + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(2.2rem, 6vw, 3.4rem); + letter-spacing: -0.025em; + line-height: 1.12; + padding-block: 0.04em; +} + +.page-hero__lede { + margin: 0; + max-width: 40rem; + color: var(--muted); + line-height: 1.55; +} + +.changelog { + position: relative; + z-index: 1; + padding-bottom: 4rem; +} + +.changelog__status { + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.changelog__status.is-error { + color: #ff8a6a; +} + +.changelog__intro { + margin: 0 0 2rem; + max-width: 44rem; + color: var(--muted); + line-height: 1.6; +} + +.cl-timeline { + list-style: none; + margin: 0; + padding: 0 0 0 1.35rem; + display: grid; + gap: 1.75rem; + border-left: 1px solid var(--line); + margin-left: 0.35rem; +} + +.cl-release { + position: relative; + padding: 0.15rem 0 0.25rem 0.15rem; + opacity: 0; + transform: translateY(14px); + transition: + opacity 0.65s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.65s cubic-bezier(0.22, 1, 0.36, 1); + transition-delay: calc(var(--i, 0) * 40ms); + overflow-wrap: anywhere; +} + +.cl-release.is-in { + opacity: 1; + transform: none; +} + +.cl-release::before { + content: ""; + position: absolute; + left: calc(-1.35rem - 0.35rem - 3px); + top: 0.85rem; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--spark); + box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.15); +} + +.cl-release__head { + display: flex; + flex-wrap: wrap; + gap: 0.55rem 1rem; + align-items: baseline; + margin-bottom: 0.85rem; +} + +.cl-ver { + font-family: var(--font-display); + font-size: 1.45rem; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--ink); + text-decoration: none; +} + +a.cl-ver:hover { + color: var(--spark); +} + +.cl-date { + font-family: var(--font-mono); + font-size: 0.82rem; + color: var(--muted); +} + +.cl-note { + margin: 0 0 1rem; + color: var(--muted); + line-height: 1.55; + max-width: 44rem; +} + +.cl-section { + margin: 0 0 1rem; + padding: 1rem 1.15rem; + border: 1px solid var(--line); + border-radius: 10px; + background: rgba(18, 18, 20, 0.65); + overflow: visible; +} + +.cl-section__title { + margin: 0 0 0.55rem; + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--spark); +} + +.cl-section--changed .cl-section__title { + color: #9ecbff; +} + +.cl-section--fixed .cl-section__title { + color: #7ee0c1; +} + +.cl-section--removed .cl-section__title { + color: #ff8a6a; +} + +.cl-list { + margin: 0; + padding-left: 1.15rem; + color: var(--muted); + line-height: 1.65; + overflow-wrap: anywhere; +} + +.cl-list li + li { + margin-top: 0.5rem; +} + +.cl-code { + font-family: var(--font-mono); + font-size: 0.88em; + color: #ffb27a; + padding: 0.14em 0.35em; + border-radius: 4px; + background: rgba(255, 106, 0, 0.08); + word-break: break-word; + line-height: 1.4; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} + +@media (max-width: 599px) { + .cursor-glow { + display: none; + } + + .topnav__links { + font-size: 0.8rem; + gap: 0.25rem 0.75rem; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + .cursor-glow, + .scroll-progress > span, + .atmosphere__field { + display: none !important; + } + + .atmosphere__spark, + .atmosphere__grid, + .hero__mark, + .hero__eyebrow, + .hero__title, + .hero__lede, + .hero__cta, + .hero__ring, + .block__copy[data-reveal], + .viz, + .viz.is-in .schem-face, + .viz.is-in .plug__node, + .viz.is-in .plug__core, + .viz.is-in .plug__out, + .viz.is-in .layers__item, + .viz.is-in .flow__step, + .viz.is-in .stores__item, + .viz.is-in .boot > li, + .viz.is-in .schem-wire, + .viz.is-in .schem-pulse, + .viz.is-in .bits li, + .viz.is-in .device-screen, + .viz.is-in .device-dot, + .viz.is-in .device-scan, + .viz.is-in .pinout__list > div, + .cl-release, + .flow__step.is-active::after { + animation: none !important; + transition: none !important; + opacity: 1 !important; + transform: none !important; + } + + .block__label::after { + width: 100%; + } + + .btn:hover { + transform: none; + } +} diff --git a/site/sync_changelog.py b/site/sync_changelog.py new file mode 100644 index 0000000..c85f1c6 --- /dev/null +++ b/site/sync_changelog.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Embed root CHANGELOG.md into site/changelog.data.js for the Pages site. + +GitHub Pages uploads only site/, so the root CHANGELOG.md is not served. +This script reads ../CHANGELOG.md and writes an embed that works under +file:// and https (no fetch/CORS). +""" + +from __future__ import annotations + +import json +from pathlib import Path + + +def main() -> int: + site = Path(__file__).resolve().parent + src = site.parent / "CHANGELOG.md" + if not src.is_file(): + raise SystemExit(f"missing {src}") + + text = src.read_text(encoding="utf-8") + out = site / "changelog.data.js" + out.write_text( + "// Generated by site/sync_changelog.py from ../CHANGELOG.md — do not edit.\n" + f"window.__FLINT_CHANGELOG_MD__ = {json.dumps(text, ensure_ascii=False)};\n", + encoding="utf-8", + ) + print(f"wrote {out.relative_to(site.parent)} from CHANGELOG.md") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())