commit 49360140af2798a208fdb874f8ec5cf900ef5f59 Author: David Young Date: Wed Oct 28 09:17:40 2026 -0600 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dffbdf --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Federated CoreDesktop — Hugo Site + +The website for [Federated CoreDesktop](https://gitea.federated.computer/Federated_Computer/coredesktop), +the scripts that turn a fresh Arch Linux + GNOME install into a tiling, +keyboard-first desktop. Its look follows the Federated Computer site +([hammerfest](https://gitea.federated.computer/Federated_Computer/hammerfest)): +a navy top bar and hero, bands that alternate white and light grey, Inter +headlines and rounded cards, recolored here with a magenta accent and a +cyan primary button. + +## Requirements + +- [Hugo](https://gohugo.io/), v0.146 or newer (tested on v0.168.0 extended). + +## Running it locally + +``` +hugo server +``` + +Then open http://localhost:1313/. + +## Building for production + +``` +hugo --minify +``` + +Output goes to `public/` (ignored by git). Links are relative, so the build +also works from any subfolder or straight from disk. + +## How pages are built + +Every page's content lives in its front matter as a `sections:` list. Each +section has a `type` plus the fields that type needs, and +`layouts/partials/render-sections.html` hands it to the matching partial in +`layouts/partials/sections/`. To change copy, edit `content/*.md`; to change +how a section type looks, edit its partial. + +| Type | Fields | +|-------------|--------| +| `hero` | `eyebrow`, `title`, `subhead`, `ctas`, `compact`. On the right: `log` (a typed-out terminal; lines take `tag`, `text`, `think`, `done`; `logTitle` sets the title bar), otherwise the three-pane tiling visual (`tiles` relabels the panes; `visual: false` drops it). `video` (a path without extension, e.g. `/video/ultrawide-desktop`, with `.webm`, `.mp4` and a `.jpg` poster in `static/`) instead runs a looping screen recording full width under the copy; `videoTitle` and `videoLabel` set its window bar and description | +| `text` | `kicker`, `headline`, `body` (Markdown, including fenced code), `ctas`. Without a headline, the body renders as a large statement; `*em*` is magenta | +| `command` | `kicker`, `headline`, `lede`, `command` (defaults to the install command), `prompt`, `note` | +| `features` | `items` of `label` + `body` | +| `cards` | `columns`, `items` of `label`, `title`, `body`, `link`, `linktext` | +| `steps` | `items` of `title` + `body` | +| `checklist` | `items` (Markdown strings) | +| `split` | `columns` of `title` + `items` (the first column is dark) | +| `stats` | `items` of `value` + `label` (numbers count up) | +| `table` | `headers`, `rows` (lists; the first cell labels the row) | +| `siblings` | The sibling directory, from `data/siblings.yaml` (see the comment at its top). `emptyText` and `emptyCtas` set what shows while it's empty | +| `faq` | `items` of `q` + `a` | +| `callout` | `text` | +| `final-cta` | `headline`, `body`, `ctas` (the navy closing band) | + +Every type also takes `kicker`, `headline`, `lede` and `id` (an anchor; +without one, it's made from the kicker or headline). `ctas` is a list of +`label`, `url` and `style` (`primary`, `ghost` or `text`). + +Code blocks and `command` sections get a Copy button from the script in +`layouts/_default/baseof.html`. + +## Where things live + +- `hugo.toml` — site settings (`params.repoUrl`, `params.installCommand`) and the menus +- `content/` — one file per page +- `data/siblings.yaml` — the projects listed in the sibling directory +- `layouts/` — templates and section partials +- `static/css/style.css` — the whole design system +- `static/video/` — the home page's screen recording (WebM, MP4 and poster) +- `static/images/coredesktop-logo.svg` — the wordmark, drawn in the Federated lettering diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..7278cc1 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,15 @@ +--- +title: "{{ replace .File.ContentBaseName "-" " " | title }}" +description: "" +sections: + - type: hero + compact: true + bars: false + eyebrow: "" + title: "{{ replace .File.ContentBaseName "-" " " | title }}" + subhead: "" + - type: text + headline: "" + body: | + +--- diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..34a19e2 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,134 @@ +--- +title: "Home" +description: "Federated CoreDesktop turns a fresh Arch Linux and GNOME install into a tiling, keyboard-first desktop with the software you need for work, sandboxed AI agents, snapshots, backups and one-command updates." +sections: + - type: hero + eyebrow: "Not a distribution, yet. Just Arch, GNOME and some very good scripts." + title: "A tiling Arch desktop, ready for work in one command." + subhead: "CoreDesktop takes a fresh **Arch Linux + GNOME** install and turns it into a keyboard-first workstation with a flexible tiling system, with the apps, developer tools, firewall, snapshots, backups and **sandboxed AI agents** already set up." + ctas: + - label: "Install CoreDesktop" + url: "/install/" + style: primary + - label: "What's included" + url: "#included" + style: ghost + video: "/video/ultrawide-desktop" + videoTitle: "CoreDesktop · 5120 × 1440 ultrawide" + videoLabel: "A CoreDesktop screen on an ultrawide monitor: three tiled windows side by side, a zoom out to the GNOME overview with the dock, and back." + + - type: text + body: "CoreDesktop is *some scripts*, not a distribution, yet. Arch Linux does the heavy lifting, so you get upstream fixes as they land, and a *flexible tiling* desktop that makes the most of any screen, *ultrawides* included." + + - type: features + id: included + kicker: "What you get" + headline: "Everything set up, nothing in the way." + lede: "One install script, safe to run again. If something fails, run it again. It skips what's already done." + items: + - label: "A tiling desktop you drive from the keyboard" + body: "GNOME with the Tiling Shell extension: a flexible tiling system you can lay out however you like, Super + arrow keys to move windows, and the full mouse-friendly desktop when you want it." + - label: "The software you'd install anyway" + body: "From pacman, the AUR, Flathub and mise: browsers, messaging, office, media, notes, remote desktop, database clients and local AI, plus a full developer toolchain." + - label: "Sandboxed AI agents" + body: "opencode, Hermes Agent, Cline Desktop and Claude Code run in a sandbox that can only change your project folder and can't read your keys or passwords." + - label: "Snapshots and backups" + body: "Snapper snapshots / before every update. `configure` adds encrypted, daily restic backups of your home folder to your Nextcloud." + - label: "Safe by default" + body: "The ufw firewall is on, Docker can't punch holes in it, and [`audit`](/utilities/#audit) shows every open port and who can reach it." + - label: "One command to stay current" + body: "`update` snapshots, then updates Arch, GNOME, AUR, Flatpaks, Ruby, extensions and firmware, and tells you if a step failed." + ctas: + - label: "Browse the software" + url: "/software/" + style: text + - label: "See the system defaults" + url: "/system/" + style: text + + - type: stats + kicker: "By the numbers" + headline: "A lot of setup you won't have to do." + items: + - value: "1" + label: "command to install, after Arch and GNOME" + - value: "48" + label: "Flatpak apps, from browsers to video editing" + - value: "4" + label: "AI coding agents, each in a sandbox" + - value: "20" + label: "steps that `update` runs, in order" + + - type: steps + kicker: "How it works" + headline: "From USB stick to working desktop." + items: + - title: "Install Arch with GNOME" + body: "Boot the Arch ISO and run `archinstall`. Choose the desktop profile, GNOME, pipewire and the right GPU driver." + - title: "Run one command" + body: "Open a terminal, paste the install command, and enter your password once. Then be patient: it takes a while." + - title: "Configure and reboot" + body: "`configure` connects your Federated Core, joins your Tailscale network, sets up backups and signs in your AI agents." + ctas: + - label: "Read the full install guide" + url: "/install/" + style: primary + + - type: command + kicker: "The install command" + headline: "Already on Arch and GNOME?" + lede: "Open a terminal and paste this in." + note: "The scripts live in `~/.local/share/coredesktop/`. Read [install.sh](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/install.sh) before you run it." + + - type: cards + kicker: "Built for Federated Computer" + headline: "Even better with your Federated Core." + lede: "CoreDesktop is what the Federated Computer team works on every day. Point it at your Core and it wires itself into your apps." + columns: 3 + items: + - label: "Accounts" + title: "Calendar, contacts, files and email" + body: "Your Nextcloud and email go into GNOME Online Accounts, your files mount in the Files sidebar, and Element points at your Core's Matrix server." + link: "/utilities/#configure" + linktext: "Connect your Core" + - label: "Network" + title: "Your own Tailscale network" + body: "Join this computer to your Core's headscale, so you can reach your other machines from anywhere." + link: "/utilities/#configure" + linktext: "Set up Tailscale" + - label: "Backups" + title: "Encrypted, daily, off this machine" + body: "restic backs up your home folder to your Nextcloud: 7 daily, 4 weekly and 6 monthly, with a passphrase only you keep." + link: "/utilities/#restoring-from-a-backup" + linktext: "How restoring works" + + - type: faq + kicker: "Questions" + headline: "Good to know before you start." + items: + - q: "Is CoreDesktop a Linux distribution?" + a: "Not yet. Today it's a set of scripts that runs on top of a standard Arch Linux and GNOME install. Arch does the heavy lifting; CoreDesktop adds the extensions, software and configuration." + - q: "Do I need a Federated Computer account?" + a: "No. Everything except the `configure` steps for your Core, its Tailscale network and backups works on its own. A lot of the software is chosen for work at Federated Computer, but you may find it useful too." + - q: "What if the install fails halfway?" + a: "Run the command again. It skips what's already done and picks up where it stopped." + - q: "Does it work on ultrawide monitors?" + a: "That's what it's best at. The tiling system adapts to any screen: pick a layout from the tiling icon in the top-right corner, or make your own." + - q: "Can I add my own tools on top?" + a: "Yes. A sibling project is any git repository with an optional `install.sh` and `update.sh`. Install it with `sibling`, and every `update` keeps it current." + ctas: + - label: "Sibling projects" + url: "/siblings/" + style: text + + - type: final-cta + headline: "Get a better desktop this afternoon." + body: "Install Arch with GNOME, paste one command, and come back to a tiling workstation that's ready for work." + ctas: + - label: "Install CoreDesktop" + url: "/install/" + style: primary + - label: "View the source" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop" + style: ghost +--- diff --git a/content/ai-sandbox.md b/content/ai-sandbox.md new file mode 100644 index 0000000..e99dcff --- /dev/null +++ b/content/ai-sandbox.md @@ -0,0 +1,94 @@ +--- +title: "AI Sandbox" +description: "Federated CoreDesktop installs opencode, Hermes Agent, Cline Desktop and Claude Code, and runs them in a sandbox that keeps them out of your keys, passwords and other files." +sections: + - type: hero + compact: true + eyebrow: "opencode · Hermes Agent · Cline Desktop · Claude Code" + title: "AI agents that can't wander off." + subhead: "CoreDesktop installs four AI coding agents and runs them in a sandbox. They can change your project, and not much else." + ctas: + - label: "Sign in your agents" + url: "/utilities/#configure" + style: primary + - label: "Sandbox rules" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/sandbox/config" + style: ghost + logTitle: "~/Projects/site $ opencode" + log: + - tag: "Sandbox" + text: "Write access: ~/Projects/site only" + - tag: "Read" + text: "Reading content/_index.md and hugo.toml" + - tag: "Write" + text: "Edited layouts/partials/header.html" + - think: true + tag: "Agent" + text: "Trying to read ~/.ssh/id_ed25519" + - tag: "Blocked" + text: "~/.ssh is hidden from the sandbox" + - tag: "Net" + text: "github.com, npm and PyPI: allowed" + - tag: "Blocked" + text: "pastebin.example: not on the allow list" + - done: true + text: "Done. 1 file changed, nothing else touched." + + - type: split + kicker: "In the sandbox" + headline: "What an agent can and can't do." + columns: + - title: "It can" + items: + - "Change files only in the directory you start it from, plus its own settings and a few package caches. So start it from a project directory." + - "Reach AI providers and common developer sites: GitHub, Gitea, npm, PyPI, crates.io, RubyGems, the Go module proxy and archlinux.org." + - title: "It can't" + items: + - "Read your SSH keys, password stores or browser profiles" + - "Read Flatpak apps' data: browsers, Signal, Telegram and the rest" + - "Read your Nextcloud password, cloud and Git credentials" + - "Read the other agents' logins" + + - type: cards + kicker: "The agents" + headline: "Four agents, one sandbox." + columns: 2 + items: + - label: "opencode" + title: "Sandboxed when you type it" + body: "Typing `opencode` runs it in the sandbox. To run it without, type `unsandboxed opencode`." + - label: "Hermes Agent" + title: "Talk to it over Matrix" + body: "`hermes` or `hermes-agent` runs in the sandbox, and so does its gateway, which lets you talk to Hermes through Matrix and other messaging apps. Its sandbox allows local sockets but hides your D-Bus session, SSH agent, Docker, Tailscale, X11, tmux and browsers." + - label: "Cline Desktop" + title: "Your projects in ~/Projects" + body: "Opened from the app menu. It can change files in `~/Projects`, open projects elsewhere only to read them, and reach cline.bot. It draws through Wayland without the GPU, D-Bus or the keyring. `unsandboxed cline-desktop` runs it without the sandbox." + - label: "Claude Code" + title: "Its own built-in sandbox" + body: "Claude Code uses its own built-in sandbox for every command it runs. See `/sandbox` inside Claude." + + - type: text + kicker: "Allow more" + headline: "When an agent needs a site or folder." + body: | + Add it in `~/.config/coredesktop/sandbox/base.json` for every agent, or in `~/.config/coredesktop/sandbox/opencode.json` (and so on) for one agent: + + ``` + { + "network": { "allowedDomains": ["docs.example.com"] }, + "filesystem": { "allowWrite": ["~/notes"] } + } + ``` + + For Hermes' gateway to reach messaging services other than the Matrix server in its settings, add their domains the same way. + + The sandbox is Anthropic's [sandbox runtime](https://github.com/anthropics/sandbox-runtime), which uses bubblewrap and socat. The default rules are in [sandbox/config/](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/sandbox/config). + + - type: final-cta + headline: "Sign in and pick a model provider." + body: "Before using an agent the first time, run `configure`." + ctas: + - label: "Configure your agents" + url: "/utilities/#configure" + style: primary +--- diff --git a/content/contribute.md b/content/contribute.md new file mode 100644 index 0000000..92bdad9 --- /dev/null +++ b/content/contribute.md @@ -0,0 +1,65 @@ +--- +title: "Contribute" +description: "Help make Federated CoreDesktop better: suggest software, send a pull request, and learn how changes reach existing machines through migrations." +sections: + - type: hero + compact: true + eyebrow: "MIT licensed · Pull requests welcome" + title: "Help make CoreDesktop better." + subhead: "Want to help? Awesome! We welcome any and all help. Suggest missing titles or services, or contribute a PR and we're off to a better CoreDesktop." + visual: false + ctas: + - label: "Open the repository" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop" + style: primary + + - type: steps + kicker: "Ways to help" + headline: "Pick one." + items: + - title: "Suggest software" + body: "Missing a title or service you use every day? Tell us, and say why." + - title: "Send a pull request" + body: "Packages live in `packages.conf`, Flatpaks in `install-flatpaks.sh`, update steps in `updates.d/` and setup in `configure.d/`." + - title: "Build a sibling" + body: "Have something that's only for you or your team? Make it a [sibling project](/siblings/) instead." + + - type: text + kicker: "Migrations" + headline: "Getting a change to existing machines." + body: | + Changes to `install.sh` only reach new installs. If existing machines need the change too, also add a migration: a script named after the current time, + + ``` + migrations/$(date +%s).sh + ``` + + that `update` runs once on each machine. It may also run on fresh installs, so make it safe to repeat. + + - type: text + id: license + kicker: "License" + headline: "MIT, the whole project." + body: | + Copyright 2026 Federated Computer, Inc. Permission is granted, free of charge, to anyone who gets a copy of this software to use, copy, modify, merge, publish, distribute, sublicense and sell it, as long as the copyright and permission notice are included. The software is provided as is, without warranty of any kind. + + Read the full text in [LiCENSE.md](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/LiCENSE.md). + + - type: text + kicker: "Thanks" + headline: "Standing on good shoulders." + body: | + Most of the heavy lifting is done by [Arch Linux](https://archlinux.org), and to that distribution we send much gratitude. Arch lets us get on with building, instead of waiting for upstream changes and fixes. + + These scripts are based on the initial work of [Typecraft](https://www.youtube.com/@typecraft_dev). + + - type: final-cta + headline: "Ready when you are." + ctas: + - label: "Install CoreDesktop" + url: "/install/" + style: primary + - label: "View the source" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop" + style: ghost +--- diff --git a/content/install.md b/content/install.md new file mode 100644 index 0000000..c33bffc --- /dev/null +++ b/content/install.md @@ -0,0 +1,91 @@ +--- +title: "Install" +description: "Install Arch Linux with GNOME, then install Federated CoreDesktop with one command." +sections: + - type: hero + compact: true + eyebrow: "About an afternoon, most of it waiting" + title: "Install Arch, then CoreDesktop." + subhead: "Two parts: a standard Arch Linux install with GNOME using `archinstall`, then one command that turns it into CoreDesktop." + tiles: ["archinstall", "install.sh", "configure"] + ctas: + - label: "Skip to the command" + url: "#installing-federated-coredesktop" + style: primary + - label: "Download Arch" + url: "https://archlinux.org/download/" + style: ghost + + - type: steps + kicker: "Part one" + headline: "Installing Arch." + lede: "Mostly choose the defaults. The choices below are the ones that matter." + items: + - title: "Download the ISO" + body: "Get the Arch ISO from [archlinux.org/download](https://archlinux.org/download/). You may need to scroll down to find the download links." + - title: "Write it to a USB stick" + body: "Any tool works. On another CoreDesktop machine, use Impression." + - title: "Boot it and run archinstall" + body: "Boot from the USB stick. When it's up, type `archinstall` to get to the simple installer. No wired connection? [Set up wifi first](#a-note-about-networking-in-arch)." + - title: "Pick the right GPU driver" + body: "**If you have an NVIDIA GPU, choose the proprietary NVIDIA option.** Choose the AMD option for AMD cards." + - title: "Choose desktop and GNOME" + body: "When asked what type of system you want, choose **Desktop**, and **GNOME** as the desktop environment." + - title: "Choose pipewire, then reboot" + body: "Pipewire is strongly recommended for sound; it works well with Jitsi, Element and the other Federated Core apps. Reboot and log in." + + - type: callout + text: "You will likely get the Arch install wrong the first or second time. Don't despair. Keep trying! The logic of the install will become clear, and frankly efficient, and you get to know the base Arch install better each time." + + - type: text + id: a-note-about-networking-in-arch + kicker: "Networking" + headline: "A note about networking in Arch." + body: | + You need an internet connection to install Arch. A wired connection should just work. On wifi, set it up with `iwctl` before running `archinstall`: + + ``` + iwctl + device list + station wlan0 scan + station wlan0 get-networks + station wlan0 connect SSID + ``` + + Replace `wlan0` with your wireless device from `device list`, and `SSID` with your network's name. Leave `iwctl` and check with `ping archlinux.org`, then carry on with the install. The Arch wiki has [the full guide](https://wiki.archlinux.org/title/Network_configuration/Wireless). + + - type: command + id: installing-federated-coredesktop + kicker: "Part two" + headline: "Installing Federated CoreDesktop." + lede: "Once Arch and GNOME are installed, log in, open a terminal and enter:" + note: "You can also find this command on [Gitea](https://gitea.federated.computer/Federated_Computer/coredesktop): open the Web browser, go to gitea.federated.computer, click Explore, then CoreDesktop." + + - type: steps + kicker: "Then" + headline: "While it runs, and after." + items: + - title: "Enter your password once" + body: "It asks once, at the start. Then be patient: it takes a while." + - title: "Run configure" + body: "At the end it offers to run `configure`, which connects your Federated Core, sets up backups and signs you in to the AI agents. If a newer kernel came in, it asks you to reboot first." + - title: "If something fails, run it again" + body: "The install skips what's already done, so running it again is always safe." + - title: "Reboot" + body: "Run `sudo reboot now` when you're done, so that Element can find the encryption keychain, among other things." + ctas: + - label: "What configure sets up" + url: "/utilities/#configure" + style: primary + - label: "What gets installed" + url: "/software/" + style: text + + - type: final-cta + headline: "Installing? Put this on." + body: "Here's a [nice song to listen to while you're installing](https://music.youtube.com/watch?v=y-5o2f9wzmw&si=_z9kcKHRwj-KTs7w)." + ctas: + - label: "Next: Configure" + url: "/utilities/#configure" + style: primary +--- diff --git a/content/siblings.md b/content/siblings.md new file mode 100644 index 0000000..c28d519 --- /dev/null +++ b/content/siblings.md @@ -0,0 +1,260 @@ +--- +title: "Siblings" +description: "Sibling projects are git repositories that build on Federated CoreDesktop. Install them with one command, keep them current with update, browse the directory, and learn how to format and submit your own." +sections: + - type: hero + compact: true + eyebrow: "sibling · sibling add · sibling list · sibling remove" + title: "Build your own layer on top." + subhead: "A sibling project is a git repository that builds on CoreDesktop. Install one with a single command, and every `update` keeps it current along with everything else." + ctas: + - label: "Browse siblings" + url: "#directory" + style: primary + - label: "Make your own" + url: "#format" + style: ghost + logTitle: "~ $ sibling add https://gitea.example.com/someone/my-tools.git" + log: + - tag: "Clone" + text: "my-tools into ~/.local/share/siblings/my-tools" + - tag: "install" + text: "Running install.sh from the repo folder" + - tag: "pacman" + text: "Installing jq and ripgrep" + - tag: "Bin" + text: "Installed ~/.local/bin/my-tools" + - done: true + text: "my-tools installed. update keeps it current." + + - type: text + kicker: "What's a sibling?" + headline: "Your tools, updated with CoreDesktop." + body: | + CoreDesktop covers what most people need. A sibling covers the rest: your team's tools, your dotfiles, the extra apps your work depends on. It's an ordinary git repository with two optional scripts at its top level, `install.sh` and `update.sh`. + + `sibling` clones the repository into `~/.local/share/siblings/` and runs its `install.sh` once. After that, every `update` pulls the latest version of each sibling and runs its `update.sh`, so the people using your project get your changes without doing anything. + + A repository needs no special files to be installable. Without the scripts, `update` just keeps the clone up to date. + + - type: command + id: install-a-sibling + kicker: "Install one" + headline: "One command to add a sibling." + lede: "It asks for the repository's git URL, clones it, runs its install.sh (if it has one), and confirms the install." + command: "sibling" + note: "Any URL or path `git clone` accepts will do. Private repositories work too, as long as you can already clone them (SSH key, credential helper or password), and every later `update` needs the same access." + + - type: table + kicker: "Commands" + headline: "Everything sibling does." + headers: ["Command", "What it does"] + rows: + - ["`sibling`", "Asks for a git URL, then installs that project"] + - ["`sibling add `", "The same, without the prompt. Run it again to repair an install"] + - ["`sibling list`", "Shows the installed siblings, their URLs and current commits"] + - ["`sibling remove `", "Deletes `~/.local/share/siblings/`. Anything its install.sh set up elsewhere stays"] + + - type: siblings + id: directory + kicker: "Directory" + headline: "Sibling projects." + lede: "Projects that build on CoreDesktop. Copy a command into a terminal to install one." + emptyText: "**No sibling projects are listed yet.** Built something on CoreDesktop? Yours could be the first." + emptyCtas: + - label: "Submit your project" + url: "#submit" + style: primary + - label: "How to format it" + url: "#format" + style: text + + - type: steps + id: format + kicker: "Make your own" + headline: "Turn a repository into a sibling." + lede: "Both scripts are optional, and both go at the top level of the repository. Other names or locations are ignored." + items: + - title: "Add install.sh" + body: "Runs once, when someone runs `sibling add`, and again if they re-run it. Use it for one-time setup: prompts, accounts, initial config." + - title: "Add update.sh" + body: "Runs on every `update`, right after the repository has been pulled. Use it to apply what changed since the last update." + - title: "Tell people how to install it" + body: "In your README, tell users to install with `sibling add `. Then [submit it](#submit) to the directory." + + - type: split + kicker: "The rules" + headline: "What each script should do." + lede: "Both run with `bash`, from the repository folder, so they don't need to be executable. A `#!/bin/bash` line keeps them runnable by hand." + columns: + - title: "install.sh" + items: + - "**Exit non-zero on failure.** `set -e` near the top is the simplest way. `sibling` then tells the user it failed and how to retry." + - "**Make it safe to re-run.** Re-running `sibling add` is how users repair an install, so skip work that's already done instead of failing on it." + - "It may prompt the user, since it runs in their terminal." + - "Install commands into `~/.local/bin` (already on `PATH`) with `install_script`." + - title: "update.sh" + items: + - "**Make it idempotent.** It runs on every `update`, usually with nothing to do." + - "**Don't prompt.** Users expect `update` to run through without questions, apart from `sudo` passwords." + - "**Exit non-zero only for real failures**, so that `update`'s error summary stays meaningful." + - "**Print what it's doing.** CoreDesktop prints only the project's name before it runs." + - "It's fine to leave it out if pulling the repository is enough, for example when `~/.local/bin` holds symlinks into the repo." + + - type: features + kicker: "Good to know" + headline: "The environment your scripts run in." + items: + - label: "Nothing is inherited" + body: "Each script runs as its own `bash` process, with no variables or functions from CoreDesktop. `$COREDESKTOP` is not set, so set it yourself: `COREDESKTOP=\"$HOME/.local/share/coredesktop\"`." + - label: "Find your own files" + body: "Locate files relative to the script, not a hard-coded path. People may clone the repository somewhere else and run install.sh by hand." + - label: "Keep pulls working" + body: "`update` pulls with `--ff-only` and never merges or resets. Never force-push the default branch, and never write into tracked files: put generated files and per-machine state in `~/.config//` or in paths listed in `.gitignore`." + - label: "sudo works as normal" + body: "`sudo` prompts for a password as usual. CoreDesktop doesn't give siblings passwordless sudo." + + - type: table + kicker: "Helpers" + headline: "CoreDesktop's helpers, from utils.sh." + lede: "Source `$COREDESKTOP/utils.sh` to use them." + headers: ["Function", "What it does"] + rows: + - ["`install_packages ...`", "Installs pacman and AUR packages with yay, skipping installed ones"] + - ["`remove_packages ...`", "Removes installed pacman and AUR packages"] + - ["`remove_flatpaks ...`", "Uninstalls Flatpaks that are installed"] + - ["`enable_services ...`", "Enables systemd services not yet enabled"] + - ["`install_script src dst`", "Copies a file atomically and makes it executable"] + - ["`is_installed pkg`", "True if a pacman package is installed"] + + - type: text + id: templates + kicker: "Templates" + headline: "Start from these." + body: | + Share the install logic between the two scripts, so packages or commands you add later reach existing machines through `update`, not just new installs. + + **install.sh** + + ``` + #!/bin/bash + # + # Purpose : Install (run by CoreDesktop's `sibling add`) + + set -e + + SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + COREDESKTOP="$HOME/.local/share/coredesktop" + source "$COREDESKTOP/utils.sh" + + # One-time setup: prompts, accounts, initial config... + mkdir -p "$HOME/.config/" + + # Everything that update.sh also needs to keep current + bash "$SELF/update.sh" + + echo " installed." + ``` + + **update.sh** + + ``` + #!/bin/bash + # + # Purpose : Keep current (run by CoreDesktop's `update`) + + set -e + + SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + COREDESKTOP="$HOME/.local/share/coredesktop" + source "$COREDESKTOP/utils.sh" + + PACKAGES=( + jq + ripgrep + ) + install_packages "${PACKAGES[@]}" + + mkdir -p "$HOME/.local/bin" + install_script "$SELF/bin/" "$HOME/.local/bin/" + ``` + + - type: text + id: testing + kicker: "Testing" + headline: "Try it in a throwaway home folder." + body: | + Point `sibling` at a local clone, with a temporary `HOME` so nothing touches your real one: + + ``` + export HOME="$(mktemp -d)" + mkdir -p "$HOME/.local/share" + ln -s /path/to/real/coredesktop "$HOME/.local/share/coredesktop" + + # Install + bash "$HOME/.local/share/coredesktop/sibling" add /path/to/sibling-repo + + # Commit a change in /path/to/sibling-repo, then run the update hook the way update does + ( source "$HOME/.local/share/coredesktop/updates.d/71-siblings.sh"; is_installed && run_update ) + + # Re-run the install to check it's safe to repeat + bash "$HOME/.local/share/coredesktop/sibling" add /path/to/sibling-repo + ``` + + Scripts that call `sudo`, `yay` or `systemctl` still change the real system, even with a temporary `HOME`. Test those on a machine or VM you don't mind changing. + + - type: text + id: custom-hooks + kicker: "Advanced" + headline: "Full control with an update hook." + body: | + Instead of `update.sh`, a project can install its own hook into CoreDesktop's `updates.d/`, which `update` runs in filename order. Use one approach or the other, not both, or the project updates twice. + + Name the hook with an unused number from **72 to 79**, so it runs after CoreDesktop's own updates but before its end-of-run steps (80 and up), and copy it in from your install.sh: + + ``` + cp updates.d/72-myproject.sh ~/.local/share/coredesktop/updates.d/ + ``` + + A hook can define `run_update`, and optionally `is_installed`, so it's skipped on machines where the project isn't installed. The full guide is in [sibling-project-updates.md](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/skills/sibling-project-updates.md). + + - type: checklist + kicker: "Checklist" + headline: "Before you share it." + items: + - "The repository can be cloned by URL by the people who will install it." + - "`install.sh` (optional) is at the top level, exits non-zero on failure, and is safe to re-run." + - "`update.sh` (optional) is at the top level, idempotent, doesn't prompt, and prints what it's doing." + - "Scripts find their own files relative to the script and set `COREDESKTOP=\"$HOME/.local/share/coredesktop\"` themselves." + - "Nothing writes to tracked files in the clone, and the default branch is never force-pushed." + - "The project doesn't also copy a hook into CoreDesktop's `updates.d/`." + - "The README tells users to install with `sibling add `." + + - type: steps + id: submit + kicker: "Submit" + headline: "Get your project in the directory." + lede: "Built a sibling other people could use? Tell us about it, and we'll add it to the directory." + items: + - title: "Check it" + body: "Go through the [checklist](#checklist). The repository must be public, so anyone can install it with `sibling add`, and should have a license." + - title: "Open an issue" + body: "Open an issue on the CoreDesktop repository titled **Sibling: your project's name**, with its git URL, a one-line description, and your name or handle." + - title: "We'll take a look" + body: "We'll install it on a test machine, and once it works, add it to the directory. We'll reply on the issue either way." + ctas: + - label: "Submit a sibling" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop/issues/new" + style: primary + - label: "Read the full format guide" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/skills/sibling-project-format.md" + style: text + + - type: final-cta + headline: "Need CoreDesktop itself changed?" + body: "If what you're building would be useful to everyone, it might belong in CoreDesktop instead of a sibling." + ctas: + - label: "Contribute to CoreDesktop" + url: "/contribute/" + style: primary +--- diff --git a/content/software.md b/content/software.md new file mode 100644 index 0000000..e8e80c9 --- /dev/null +++ b/content/software.md @@ -0,0 +1,104 @@ +--- +title: "Software" +description: "What Federated CoreDesktop installs: pacman packages, AUR packages, 48 Flatpaks, Ruby and Rails with mise, and a full set of development tools." +sections: + - type: hero + compact: true + eyebrow: "pacman · AUR · Flathub · mise" + title: "The software you'd install anyway." + subhead: "CoreDesktop installs a lot of useful software, mostly for work at Federated Computer, but you may find it useful too. Missing something? Suggest it, or send in a PR." + tiles: ["neovim", "Firefox", "Element"] + ctas: + - label: "Suggest a title" + url: "/contribute/" + style: primary + + - type: table + kicker: "Four sources" + headline: "Where the software comes from." + lede: "Each source has its own list in the repository, so you can see exactly what's installed." + headers: ["Source", "What it installs", "Where it's listed"] + rows: + - ["pacman", "Arch's own packages: neovim, tmux, Go, Rust, Docker, Tailscale, Element, LibreOffice and more", "[packages.conf](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/packages.conf)"] + - ["AUR", "Community packages, built and kept up to date with yay: Helium Browser, Winboat and more", "[packages.conf](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/packages.conf)"] + - ["Flathub", "48 desktop apps in their own sandboxes", "[install-flatpaks.sh](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/install-flatpaks.sh)"] + - ["mise", "Ruby and Rails", "[install-mise-ruby-rails.sh](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/install-mise-ruby-rails.sh)"] + + - type: cards + kicker: "From Flathub" + headline: "48 desktop apps, ready on first login." + lede: "Want more? Visit [flathub.org](https://flathub.org)." + columns: 3 + items: + - label: "Browsers" + title: "Every browser you test against" + body: "Chrome, Chromium, Firefox, Brave, Zen and Vivaldi, plus Helium from the AUR as the default. Pick another in Settings › Apps › Default Apps." + - label: "Messaging" + title: "Talk to anyone" + body: "Signal, Telegram, Discord, Revolt and FluffyChat, Element from pacman, and Zoom for meetings." + - label: "Email & notes" + title: "Write it down" + body: "Thunderbird and Evolution for email; Obsidian, Logseq and Xournal++ for notes; Apostrophe for Markdown writing." + - label: "Media" + title: "Make and play media" + body: "Kdenlive for video editing, OBS for recording, Audacity for audio, Inkscape for vector graphics, Scribus for page layout, Spotify and Jellyfin." + - label: "Developers" + title: "Code and data" + body: "VS Code and VSCodium (VS Code without the Microsoft telemetry), Postman, DBeaver and Beekeeper Studio." + - label: "Local AI" + title: "Models on your own machine" + body: "Alpaca and LM Studio run language models locally, with nothing leaving your computer." + - label: "Remote" + title: "Reach other machines" + body: "Remmina and RustDesk for remote desktops, LocalSend to send files to nearby devices, and Trayscale for Tailscale." + - label: "Utilities" + title: "The useful little things" + body: "Boxes for virtual machines, Impression to write ISOs to USB sticks, Warehouse to manage Flatpaks, Extension Manager for GNOME, and Helvum for audio routing." + - label: "And more" + title: "Downtime included" + body: "qBittorrent, RetroArch and others. See [install-flatpaks.sh](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/install-flatpaks.sh) for the full list." + + - type: split + kicker: "Development tools" + headline: "A full toolchain, out of the box." + lede: "Everything you need to build, run and ship, including three terminals: the GNOME console, kitty and ghostty." + columns: + - title: "Languages & runtimes" + items: + - "Go, Rust, Python, Node.js, npm and Bun" + - "Ruby and Rails, through mise" + - "Hugo and Dart Sass for websites" + - "Docker, Docker Compose and lazydocker" + - "pandoc and TeX Live for documents" + - title: "In the terminal" + items: + - "neovim (with kickstart.nvim), vim and tmux (with tpm)" + - "herdr, lazygit and difftastic" + - "ripgrep, fd, fzf, bat, eza, zoxide and yazi" + - "btop, htop, fastfetch and hyperfine" + - "nmap, mtr, sniffnet and whois" + + - type: checklist + kicker: "On the desktop" + headline: "Also from pacman and the AUR." + items: + - "**LibreOffice** for documents, spreadsheets and slides" + - "**Bitwarden** for passwords" + - "**Element** for Matrix chat" + - "**GIMP**, **Flameshot** and **ImageMagick** for images" + - "**VLC** and **mpv** for video" + - "**Winboat** to run Windows apps" + - "**Cline Desktop**, in the AI sandbox" + - "**GNOME Tweaks** and the extra GNOME apps" + + - type: final-cta + headline: "Something missing?" + body: "Tell us which titles or services you'd add, or send in a pull request." + ctas: + - label: "How to contribute" + url: "/contribute/" + style: primary + - label: "packages.conf" + url: "https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/packages.conf" + style: ghost +--- diff --git a/content/system.md b/content/system.md new file mode 100644 index 0000000..e450658 --- /dev/null +++ b/content/system.md @@ -0,0 +1,106 @@ +--- +title: "System" +description: "How Federated CoreDesktop sets up your system: tiling, the shell, the firewall, Snapper snapshots, GPU drivers, mDNS, printing, open-port audits and sensible defaults." +sections: + - type: hero + compact: true + eyebrow: "Sensible defaults, all of them explained" + title: "What CoreDesktop sets up for you." + subhead: "A flexible tiling GNOME desktop, a better shell, a firewall that's actually on, snapshots before every update, and a handful of small fixes that add up." + tiles: ["Tiling", "Firewall", "Snapshots"] + + - type: text + kicker: "Tiling" + headline: "Flexible tiling, all from the keyboard." + body: | + You end up with a GNOME system with a flexible tiling setup that works on any screen and really shines on widescreens. Hold Super (the Windows key or equivalent) and use the arrow keys to move windows around the screen. + + To change the tiling, use the icon in the top right of your screen. You can set it up however you want. + + - type: text + kicker: "Shell" + headline: "A better terminal, without the setup." + body: | + Your `~/.bashrc` sources `~/.local/share/coredesktop/shell/bashrc`, which sets up: + + - the **starship** prompt (your own `~/.config/starship.toml` wins if you have one) + - **zoxide**: `z {part of a path}` jumps to a directory you've used before + - **fzf** key bindings: Ctrl+R searches history, Ctrl+T inserts a file path + - **eza** for `ls`, `ll`, `la` and `lt` + - the aliases and functions in [shell/](https://gitea.federated.computer/Federated_Computer/coredesktop/src/branch/main/shell) + + Anything you add to `~/.bashrc` after the CoreDesktop line overrides it. + + - type: text + kicker: "Firewall" + headline: "ufw is on, and Docker can't get around it." + body: | + Incoming connections are blocked except SSH (22), mDNS (5353/udp), LocalSend (53317) and anything arriving over Tailscale. + + Docker normally publishes container ports around ufw, so **ufw-docker** is installed too. To let a container's port through, or to open a port for something else, such as a dev server you want to reach from another machine: + + ``` + sudo ufw-docker allow {container} {port} + sudo ufw allow {port}/tcp + sudo ufw status + ``` + + To see every open port and who can reach it, run [audit](/utilities/#audit). + + - type: text + id: snapshots + kicker: "Snapshots" + headline: "Roll back a bad update." + body: | + If `/` is btrfs (the archinstall default), Snapper takes a snapshot of `/` before every `update` and keeps the last 5. `/home` is a separate subvolume and isn't included, so rolling back never touches your files. `sudo snapper -c root list` shows the snapshots. + + These snapshots can't be picked from the boot menu. To roll back, boot the Arch ISO and replace the `@` subvolume with a snapshot: + + ``` + mount -o subvolid=5 /dev/{root partition} /mnt + mv /mnt/@ /mnt/@.broken + btrfs subvolume snapshot /mnt/@.snapshots/{number}/snapshot /mnt/@ + reboot + ``` + + If the disk is encrypted, open it with `cryptsetup open` first and mount the opened device. Once the system works again, free the space the broken root used: + + ``` + sudo mount -o subvolid=5 /dev/{root partition} /mnt + sudo btrfs subvolume delete /mnt/@.broken + sudo umount /mnt + ``` + + - type: features + kicker: "Hardware & network" + headline: "Drivers, printing and your LAN." + items: + - label: "GPU drivers" + body: "The Vulkan driver for Intel and AMD GPUs. For NVIDIA, if archinstall's proprietary driver wasn't chosen: the open kernel modules for GTX 16xx / RTX 20xx and newer, or the 580xx legacy driver back to the GTX 900 series. Reboot afterwards." + - label: "mDNS" + body: "Reach machines on your LAN as `machine.local`, e.g. `ssh machine.local`. Avahi and nss-mdns are set up ahead of systemd-resolved, so the two don't conflict." + - label: "Printing" + body: "The CUPS printing service is turned on. This may need more work to just work with other CoreDesktop machines." + + - type: checklist + kicker: "System defaults" + headline: "Small fixes that add up." + items: + - "SSH connections notice a dropped link within a minute instead of hanging (override per host in `~/.ssh/config`)." + - "Logins allow 10 wrong passwords before locking the account for 2 minutes. Arch's default is 3 tries, then 10 minutes." + - "Shutdown waits at most 10 seconds for a service that won't stop, instead of 90." + - "If memory runs out, systemd-oomd closes the app causing it, instead of the whole desktop freezing." + - "Boot no longer waits for the network." + - "Commands run over SSH (`ssh {machine} {command}`) can find tools in `~/.local/bin` and mise." + + - type: callout + text: "On a machine without systemd-resolved, set up mDNS yourself with `bash ~/.local/share/coredesktop/fixmdns.sh`. See the [Arch wiki](https://wiki.archlinux.org/title/Avahi#Hostname_resolution) for more." + + - type: final-cta + headline: "Keep it all current with one command." + body: "`update` snapshots first, then updates everything, and tells you what failed." + ctas: + - label: "How updates work" + url: "/utilities/#update" + style: primary +--- diff --git a/content/utilities.md b/content/utilities.md new file mode 100644 index 0000000..661bc7c --- /dev/null +++ b/content/utilities.md @@ -0,0 +1,215 @@ +--- +title: "Utilities" +description: "The CoreDesktop commands: update, refresh, version, audit, configure and reset. Keep your system current, check your version and open ports, connect your Federated Core and back up, or start over." +sections: + - type: hero + compact: true + eyebrow: "update · refresh · version · audit · configure · reset" + title: "A few commands keep it all running." + subhead: "CoreDesktop comes with a handful of utilities. Type one in any terminal to keep your system current, see what's open, connect your Core, or start over." + logTitle: "CoreDesktop utilities" + log: + - tag: "update" + text: "Snapshot, then update everything" + - tag: "refresh" + text: "Pull in new CoreDesktop additions" + - tag: "version" + text: "Show what this machine is running" + - tag: "audit" + text: "Show open ports and who can reach them" + - tag: "configure" + text: "Connect your Core, Tailscale and backups" + - tag: "reset" + text: "Back to the state right after install" + - done: true + text: "Type any of them in a terminal." + + - type: cards + kicker: "At a glance" + headline: "Six commands, one job each." + columns: 3 + items: + - label: "update" + title: "Keep everything current" + body: "Snapshots your system, then updates Arch, GNOME, AUR packages, Flatpaks, Ruby, extensions and firmware." + link: "/utilities/#update" + linktext: "How update works" + - label: "refresh" + title: "Get what's new" + body: "Runs the install script again to add any new applications or services." + link: "/utilities/#refresh" + linktext: "About refresh" + - label: "version" + title: "Know what you're running" + body: "Shows your CoreDesktop version, kernel and last update, and whether something newer is waiting." + link: "/utilities/#version" + linktext: "About version" + - label: "audit" + title: "See who can reach you" + body: "Lists every open network port, who can connect to it through the firewall, and the service behind it." + link: "/utilities/#audit" + linktext: "About audit" + - label: "configure" + title: "Sign in once" + body: "Connects your Federated Core, joins its Tailscale network, sets up backups and signs in your AI agents." + link: "/utilities/#configure" + linktext: "About configure" + - label: "reset" + title: "Start over, cleanly" + body: "Changes your installation back to the state it was in right after you installed it." + link: "/utilities/#reset" + linktext: "About reset" + + - type: command + id: update + kicker: "update" + headline: "Keep your system up to date." + lede: "From time to time, you'll want to update your system. Hopefully you'll do it more than once a month." + command: "update" + note: "It's a good idea to reboot after an update, especially when a new Linux kernel comes in, which happens often enough on Arch." + + - type: checklist + kicker: "What update does" + headline: "Each step in updates.d, in order." + lede: "update pulls the latest CoreDesktop, then runs each step in `~/.local/share/coredesktop/updates.d/`. If a step fails, the other steps still run, and the failed ones are listed at the end." + items: + - "Stops if there's less than 10 GiB free on `/`. Run `UPDATE_FORCE=1 update` to skip the check." + - "Snapshots `/` first, if [Snapper is set up](/system/#snapshots)." + - "Updates the Arch keyring, then Arch and GNOME, then AUR packages, Flatpaks, Ruby, CoreDesktop's pipx apps, GNOME extensions and firmware." + - "Warns you not to reboot if the initramfs failed to rebuild." + - "Runs any one-time migrations, the way existing machines pick up new CoreDesktop setup." + - "Removes packages installed only as dependencies that nothing needs anymore, and lists them. Packages CoreDesktop installs are never removed this way." + + - type: callout + text: "To keep a package you use directly from being removed, run `sudo pacman -D --asexplicit `. And if an update ever barfs, check the **Latest News** on [archlinux.org](https://archlinux.org) for fixes." + + - type: command + id: refresh + kicker: "refresh" + headline: "Pull in new additions." + lede: "refresh downloads the latest CoreDesktop and runs through the original install script again, adding any new applications or services. Anything already installed is skipped." + command: "refresh" + note: "Run `update` after `refresh`." + + - type: text + id: version + kicker: "version" + headline: "Which version am I running?" + body: | + CoreDesktop is a rolling release, so its version is the date of the latest CoreDesktop change on your machine, followed by that change's id: + + ``` + $ version + CoreDesktop 2026.10.22 (c5f0651) + Kernel 7.2.11-arch1-1 + Last update 2026-10-22 13:43 + ``` + + The kernel comes from Arch, so it's shown separately. version also tells you: + + - if a newer version of CoreDesktop is available + - if a newer kernel is installed and waiting for a reboot + - if your copy of CoreDesktop has local changes (it says "modified") + + In a script, `version --check` prints the newer version if there is one. + + ### You don't have to check by hand + + A few minutes after you log in, and every few hours after that, CoreDesktop checks for a new version and shows a notification if there is one. Click it to run update in a terminal, or dismiss it. Once dismissed, you won't be notified again until there's an even newer version. + + - type: text + id: audit + kicker: "audit" + headline: "See who can reach your machine." + body: | + ``` + audit + ``` + + audit shows the network ports open on this computer. For each one it shows who can connect after [the firewall](/system/#firewall): only this computer, your Tailscale network, or your LAN. It also names the service that opened the port and explains why, for example SSH, printing, mDNS, Tailscale or LocalSend. + + You can stop a service from there, which also keeps it from starting at boot, or start a stopped one again. update leaves the services you stop off. audit won't stop the ones the desktop, Files or the network need. + + Run it with sudo rights so it can name the services run by root. `audit --list` only shows the ports. + + - type: command + id: configure + kicker: "configure" + headline: "Sign in once, and it's all connected." + lede: "Some of what CoreDesktop installs needs you to sign in or make a choice first. configure shows what's set up and sets up the rest." + command: "configure" + note: "To set up one thing, name it, e.g. `configure hermes`. `configure --list` shows the names." + + - type: features + kicker: "What configure covers" + headline: "Your Core, your network, your backups, your agents." + items: + - label: "Your Federated Computer Core" + body: "Give it your Superpanel's address (e.g. https://superpanel.example.com), your Core email, name and password. It adds Superpanel to your apps, your Nextcloud calendar, contacts and files and your email to GNOME Online Accounts, mounts your Nextcloud files at `/mnt/example.com`, and points Element at your Core's Matrix server." + - label: "Your Core's Tailscale network" + body: "Joins this computer to it through `headscale.example.com`, so you can reach your other machines. Set up your Core first. You sign in through a link it shows you; if this computer is on another Tailscale network, it moves it to your Core's." + - label: "Daily backups of your home folder" + body: "Encrypted restic backups to a `.backup-{machine name}` folder in your Nextcloud files. It keeps 7 daily, 4 weekly and 6 monthly backups, and leaves out caches and things that can be reinstalled." + - label: "Your AI agents" + body: "Signs you in to Claude Code, opencode and Hermes Agent, and lets you choose their model providers. See the [AI Sandbox](/ai-sandbox/)." + + - type: callout + text: "**Save your backup passphrase.** configure shows it once. Keep it somewhere other than this computer, such as Bitwarden. Without it, the backups can't be restored." + + - type: text + id: backups + kicker: "Backups" + headline: "How the backups work." + body: | + restic reaches Nextcloud directly through rclone, with its own Nextcloud app password, so the backup doesn't depend on the `/mnt` mount. The Nextcloud app passwords configure creates can be revoked in Nextcloud under **Settings › Security**. + + To leave out more, list paths in `~/.config/coredesktop/backup-exclude`, one per line. + + ``` + systemctl --user list-timers coredesktop-backup.timer + journalctl --user -u coredesktop-backup + ``` + + The first shows when the next backup runs; the second shows how the last ones went. + + - type: text + id: restoring-from-a-backup + kicker: "Restoring" + headline: "Restoring from a backup." + body: | + To see the backups and get files back: + + ``` + source ~/.local/share/coredesktop/system/backup-env.sh + restic snapshots + restic restore latest --target ~/restored --include ~/Documents + ``` + + ### On a new computer + + Run `configure core` and `configure backup` first. Then name the old computer's folder, and restic asks for the passphrase you saved: + + ``` + source ~/.local/share/coredesktop/system/backup-env.sh .backup-{old machine name} + restic snapshots + ``` + + - type: command + id: reset + kicker: "reset" + headline: "Start over from a clean install." + lede: "reset changes your CoreDesktop installation back to the state it was in right after you installed it. Use it when you've changed so much that starting again from a known-good setup is simpler than undoing things one by one." + command: "reset" + note: "Before you reset, make sure your [backup](/utilities/#backups) is current. Afterwards, run `update` to bring everything up to date." + + - type: final-cta + headline: "Don't have a Federated Core yet?" + body: "Every app your business needs, unlimited users, one low price, and private AI that runs on it." + ctas: + - label: "Visit Federated Computer" + url: "https://federated.computer/" + style: primary + - label: "Contribute to CoreDesktop" + url: "/contribute/" + style: ghost +--- diff --git a/data/siblings.yaml b/data/siblings.yaml new file mode 100644 index 0000000..a0419fb --- /dev/null +++ b/data/siblings.yaml @@ -0,0 +1,20 @@ +# Sibling projects shown in the directory on /siblings/. +# +# One entry per project. `name`, `url` and `description` are required; the +# rest are optional. `url` is what users pass to `sibling add`, so it must be +# cloneable by the people who will install it. +# +# - name: "my-tools" +# url: "https://gitea.example.com/someone/my-tools.git" +# description: "One line on what it adds to CoreDesktop." +# author: "Someone" +# authorUrl: "https://example.com" +# homepage: "https://gitea.example.com/someone/my-tools" # defaults to url without .git +# tags: ["development", "gnome"] + +- name: "HamPack" + url: "https://gitea.young.computer/david/HamPack.git" + description: "Everything you need to turn CoreDesktop into a great ham radio computer: fldigi, flrig, WSJT-X, QLog, CHIRP, GridTracker, Pat for Winlink and more, plus VARA through Wine. `hampackremove` uninstalls it all." + author: "david" + authorUrl: "https://gitea.young.computer/david" + tags: ["ham radio", "digital modes", "logging"] diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..52a174d --- /dev/null +++ b/hugo.toml @@ -0,0 +1,117 @@ +baseURL = "https://coredesktop.computer/" +relativeURLs = true +locale = "en-US" +title = "Federated CoreDesktop" + +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT"] + +[params] + tagline = "A keyboard-first Arch Linux and GNOME desktop, ready for work." + repoUrl = "https://gitea.federated.computer/Federated_Computer/coredesktop" + installCommand = "wget -qO- https://gitea.federated.computer/Federated_Computer/coredesktop/raw/branch/main/install.sh | bash" + companyUrl = "https://federated.computer/" + +[markup.goldmark.renderer] + unsafe = true + +# Fenced code renders as plain
, styled as a terminal in style.css.
+[markup.highlight]
+  codeFences = false
+
+[[menu.main]]
+  name = "Install"
+  url = "/install/"
+  weight = 10
+
+[[menu.main]]
+  name = "Software"
+  url = "/software/"
+  weight = 20
+
+[[menu.main]]
+  name = "System"
+  url = "/system/"
+  weight = 30
+
+[[menu.main]]
+  name = "AI Sandbox"
+  url = "/ai-sandbox/"
+  weight = 40
+
+[[menu.main]]
+  name = "Utilities"
+  url = "/utilities/"
+  weight = 50
+
+[[menu.main]]
+  name = "Siblings"
+  url = "/siblings/"
+  weight = 55
+
+[[menu.main]]
+  name = "Contribute"
+  url = "/contribute/"
+  weight = 70
+
+[[menu.footer_guide]]
+  name = "Install Arch + CoreDesktop"
+  url = "/install/"
+  weight = 10
+[[menu.footer_guide]]
+  name = "Configure"
+  url = "/utilities/#configure"
+  weight = 20
+[[menu.footer_guide]]
+  name = "Keep it up to date"
+  url = "/utilities/#update"
+  weight = 30
+[[menu.footer_guide]]
+  name = "Restore from a backup"
+  url = "/utilities/#restoring-from-a-backup"
+  weight = 40
+[[menu.footer_guide]]
+  name = "Reset to a fresh install"
+  url = "/utilities/#reset"
+  weight = 50
+
+[[menu.footer_desktop]]
+  name = "Software"
+  url = "/software/"
+  weight = 10
+[[menu.footer_desktop]]
+  name = "System"
+  url = "/system/"
+  weight = 20
+[[menu.footer_desktop]]
+  name = "AI Sandbox"
+  url = "/ai-sandbox/"
+  weight = 30
+[[menu.footer_desktop]]
+  name = "Utilities"
+  url = "/utilities/"
+  weight = 40
+[[menu.footer_desktop]]
+  name = "Sibling projects"
+  url = "/siblings/"
+  weight = 50
+[[menu.footer_desktop]]
+  name = "Submit a sibling"
+  url = "/siblings/#submit"
+  weight = 60
+
+[[menu.footer_project]]
+  name = "Source on Gitea"
+  url = "https://gitea.federated.computer/Federated_Computer/coredesktop"
+  weight = 10
+[[menu.footer_project]]
+  name = "Contribute"
+  url = "/contribute/"
+  weight = 20
+[[menu.footer_project]]
+  name = "MIT License"
+  url = "/contribute/#license"
+  weight = 30
+[[menu.footer_project]]
+  name = "Federated Computer"
+  url = "https://federated.computer/"
+  weight = 40
diff --git a/layouts/404.html b/layouts/404.html
new file mode 100644
index 0000000..de398a0
--- /dev/null
+++ b/layouts/404.html
@@ -0,0 +1,10 @@
+{{ define "main" }}
+  {{ partial "sections/hero.html" (dict
+    "compact" true
+    "visual" false
+    "eyebrow" "404"
+    "title" "That page isn't here."
+    "subhead" "It may have moved. Try the install guide, or head back home."
+    "ctas" (slice (dict "label" "Back home" "url" "/" "style" "primary") (dict "label" "Install guide" "url" "/install/" "style" "ghost"))
+  ) }}
+{{ end }}
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
new file mode 100644
index 0000000..2680cec
--- /dev/null
+++ b/layouts/_default/baseof.html
@@ -0,0 +1,54 @@
+
+
+
+  {{ partial "head.html" . }}
+
+
+  
+  {{ partial "header.html" . }}
+  
+ {{ block "main" . }}{{ end }} +
+ {{ partial "footer.html" . }} + + + diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..2a53838 --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,3 @@ +{{ define "main" }} + {{ partial "render-sections.html" . }} +{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..2a53838 --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,3 @@ +{{ define "main" }} + {{ partial "render-sections.html" . }} +{{ end }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 0000000..878d217 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,32 @@ +
+
+
+
+ + + +

Scripts that turn a fresh Arch Linux and GNOME install into a tiling, keyboard-first desktop, ready for work.

+ Built on Arch Linux · MIT licensed +
+ +
+ {{ range (slice (dict "title" "Guide" "menu" "footer_guide") (dict "title" "The Desktop" "menu" "footer_desktop") (dict "title" "Project" "menu" "footer_project")) }} + + {{ end }} +
+
+ + +
+
diff --git a/layouts/partials/head.html b/layouts/partials/head.html new file mode 100644 index 0000000..1e34f3c --- /dev/null +++ b/layouts/partials/head.html @@ -0,0 +1,12 @@ + + +{{ if .IsHome }}{{ .Site.Title }} — {{ .Site.Params.tagline }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }} + + + + + + + + + diff --git a/layouts/partials/header.html b/layouts/partials/header.html new file mode 100644 index 0000000..db91c44 --- /dev/null +++ b/layouts/partials/header.html @@ -0,0 +1,37 @@ +{{/* + The top bar: logo, the site's pages, and the two actions (source and + install). Sticky on every screen; below 1080px the pages fold into a Menu + toggle (a
, so it works without JS). +*/}} +{{ $page := . }} +
+
+ + + + + + + + + +
+
diff --git a/layouts/partials/render-sections.html b/layouts/partials/render-sections.html new file mode 100644 index 0000000..02be74f --- /dev/null +++ b/layouts/partials/render-sections.html @@ -0,0 +1,9 @@ +{{ range .Params.sections }} + {{ if eq .type "hero" }} + {{ partial "sections/hero.html" . }} + {{ else if eq .type "final-cta" }} + {{ partial "sections/final-cta.html" . }} + {{ else }} + {{ partial "sections/wrapper.html" . }} + {{ end }} +{{ end }} diff --git a/layouts/partials/section-id.html b/layouts/partials/section-id.html new file mode 100644 index 0000000..fe6d0ac --- /dev/null +++ b/layouts/partials/section-id.html @@ -0,0 +1,12 @@ +{{/* + Anchor id for a section: its own `id`, else one made from its `toc`, + `kicker`, or `headline`. Used by the section wrapper and by the pricing + switch. +*/}} +{{ $id := "" }} +{{ with .id }} + {{ $id = . }} +{{ else }} + {{ with (or .toc .kicker .headline) }}{{ $id = . | plainify | anchorize }}{{ end }} +{{ end }} +{{ return $id }} diff --git a/layouts/partials/sections/callout.html b/layouts/partials/sections/callout.html new file mode 100644 index 0000000..d67005c --- /dev/null +++ b/layouts/partials/sections/callout.html @@ -0,0 +1,4 @@ +{{ $s := . }} +{{ partial "sections/head.html" $s }} +{{ with $s.intro }}
{{ . | markdownify }}
{{ end }} +

{{ $s.text | markdownify }}

diff --git a/layouts/partials/sections/cards.html b/layouts/partials/sections/cards.html new file mode 100644 index 0000000..be065e7 --- /dev/null +++ b/layouts/partials/sections/cards.html @@ -0,0 +1,22 @@ +{{/* + A grid of cards: `items`, each with `title`, `body`, optional `label` + (a small tag above the title), `image`, and `link` (+ `linktext`). + Without an image, `visual: true` draws a gradient panel in its place. +*/}} +{{ $s := . }} +{{ partial "sections/head.html" $s }} + +{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/checklist.html b/layouts/partials/sections/checklist.html new file mode 100644 index 0000000..f2e78e8 --- /dev/null +++ b/layouts/partials/sections/checklist.html @@ -0,0 +1,5 @@ +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
    + {{ range $s.items }}
  • {{ . | markdownify }}
  • {{ end }} +
diff --git a/layouts/partials/sections/command.html b/layouts/partials/sections/command.html new file mode 100644 index 0000000..5c63780 --- /dev/null +++ b/layouts/partials/sections/command.html @@ -0,0 +1,16 @@ +{{/* + A single command in a terminal window, with a Copy button (added by the + script in baseof.html). `command` defaults to the site's install + command; `prompt` sets the title bar; `note` goes under the window. +*/}} +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
+
+ + {{ $s.prompt | default "Terminal" }} +
+
{{ $s.command | default site.Params.installCommand }}
+
+{{ with $s.note }}

{{ . | markdownify }}

{{ end }} +{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/ctas.html b/layouts/partials/sections/ctas.html new file mode 100644 index 0000000..a2039d9 --- /dev/null +++ b/layouts/partials/sections/ctas.html @@ -0,0 +1,17 @@ +{{/* + Buttons. `primary` is the solid cyan button, `ghost` the outlined + secondary, `text` a quiet link with an arrow. Any trailing "→" in a + label is dropped — the arrow is drawn. +*/}} +{{ range . }} + {{ $label := replaceRE `\s*[→↗]\s*$` "" .label }} + {{ $external := or (hasPrefix .url "http") (hasPrefix .url "mailto:") }} + {{ $href := cond (or $external (hasPrefix .url "#")) .url (.url | relURL) }} + {{ if eq .style "ghost" }} + {{ $label }} + {{ else if eq .style "text" }} + {{ $label }} + {{ else }} + {{ $label }} + {{ end }} +{{ end }} diff --git a/layouts/partials/sections/faq.html b/layouts/partials/sections/faq.html new file mode 100644 index 0000000..c39f193 --- /dev/null +++ b/layouts/partials/sections/faq.html @@ -0,0 +1,11 @@ +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
+ {{ range $s.items }} +
+ {{ .q }} +

{{ .a | markdownify }}

+
+ {{ end }} +
+{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/features.html b/layouts/partials/sections/features.html new file mode 100644 index 0000000..e267ab2 --- /dev/null +++ b/layouts/partials/sections/features.html @@ -0,0 +1,8 @@ +{{ $s := . }} +{{ partial "sections/head.html" $s }} +{{ with $s.intro }}
{{ . | markdownify }}
{{ end }} +
    + {{ range $s.items }} +
  • {{ .label }}{{ .body | markdownify }}
  • + {{ end }} +
diff --git a/layouts/partials/sections/final-cta.html b/layouts/partials/sections/final-cta.html new file mode 100644 index 0000000..03f42b1 --- /dev/null +++ b/layouts/partials/sections/final-cta.html @@ -0,0 +1,10 @@ +{{ $s := . }} + diff --git a/layouts/partials/sections/head.html b/layouts/partials/sections/head.html new file mode 100644 index 0000000..5f83dbd --- /dev/null +++ b/layouts/partials/sections/head.html @@ -0,0 +1,8 @@ +{{/* Shared section heading: optional kicker (a mono eyebrow), headline, and lede. */}} +{{ if or .kicker .headline .lede }} +
+ {{ with .kicker }}

{{ . }}

{{ end }} + {{ with .headline }}

{{ . | markdownify }}

{{ end }} + {{ with .lede }}

{{ . | markdownify }}

{{ end }} +
+{{ end }} diff --git a/layouts/partials/sections/hero.html b/layouts/partials/sections/hero.html new file mode 100644 index 0000000..1ebf6ea --- /dev/null +++ b/layouts/partials/sections/hero.html @@ -0,0 +1,139 @@ +{{/* + The opening of every page: a full-bleed dark band. On the left, an + optional note (the eyebrow), the statement, the dek, the actions; on the + right, a terminal typing out an install (when `log:` is set), or the + tiles — a three-pane tiled desktop, CoreDesktop's default layout. + `visual: false` drops the tiles; `tiles:` relabels the three panes. + `video:` (a path without extension, with .webm, .mp4 and .jpg beside it) + instead runs a looping screen recording full width under the copy; + `videoTitle` sets its window bar and `videoLabel` its description. +*/}} +{{ $s := . }} +{{ $visual := and (not $s.video) (or $s.log (ne $s.visual false)) }} +
+
+
+ {{ with $s.eyebrow }}

{{ . }}

{{ end }} +

{{ $s.title | markdownify }}

+ {{ with $s.subhead }}

{{ . | markdownify }}

{{ end }} + {{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} +
+ + {{ if not $s.video }} + {{ with $s.log }} + {{/* + A terminal, typed out line by line (see the script below). Lines take + `tag` + `text`; `think: true` marks a waiting step (greyed, with a + spinner); `done: true` closes the run. The window keeps a fixed height + and scrolls as lines arrive. + */}} +
+
+
+ + {{ $s.logTitle | default "~ $ install.sh" }} + Running +
+
+
    + {{ range . }} +
  1. + {{ .tag | default (cond .done "Done" (cond (.think | default false) "Wait" "")) }} + {{ .text }} +
  2. + {{ end }} +
+
+
+
+ + {{ else }} + {{ if ne $s.visual false }} + {{ $tiles := $s.tiles | default (slice "Editor" "Terminal" "Browser") }} + + {{ end }} + {{ end }} + {{ end }} +
+ {{ with $s.video }} + {{/* + Muted, looping and inline, so it can autoplay. With reduced motion it + stays on the poster, with controls to play it (see the script below). + */}} +
+
+
+ + {{ $s.videoTitle | default "CoreDesktop" }} +
+ +
+
+ + {{ end }} +
diff --git a/layouts/partials/sections/siblings.html b/layouts/partials/sections/siblings.html new file mode 100644 index 0000000..5452699 --- /dev/null +++ b/layouts/partials/sections/siblings.html @@ -0,0 +1,33 @@ +{{/* + The sibling project directory, from data/siblings.yaml. Each card shows the + project, its author and tags, and the `sibling add` command to install it + (with a Copy button, from baseof.html). With no projects listed yet, an + empty card invites the first submission: `emptyText` and `emptyCtas`. +*/}} +{{ $s := . }} +{{ $projects := hugo.Data.siblings | default slice }} +{{ partial "sections/head.html" $s }} +{{ if $projects }} +
    + {{ range sort $projects "name" }} + {{ $p := . }} + {{ $home := .homepage | default (replaceRE `\.git$` "" .url) }} +
  • +
    +

    {{ .name }}

    + {{ with $p.author }}by {{ with $p.authorUrl }}{{ $p.author }}{{ else }}{{ . }}{{ end }}{{ end }} +
    +

    {{ .description | markdownify }}

    + {{ with .tags }}
      {{ range . }}
    • {{ . }}
    • {{ end }}
    {{ end }} +
    sibling add {{ .url }}
    +
  • + {{ end }} +
+{{ else }} +
+ +

{{ $s.emptyText | default "No sibling projects are listed yet. Yours could be the first." | markdownify }}

+ {{ with $s.emptyCtas }}
{{ partial "sections/ctas.html" . }}
{{ end }} +
+{{ end }} +{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/split.html b/layouts/partials/sections/split.html new file mode 100644 index 0000000..294f405 --- /dev/null +++ b/layouts/partials/sections/split.html @@ -0,0 +1,14 @@ +{{/* Two columns, each a heading plus a short list. */}} +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
+ {{ range $s.columns }} +
+

{{ .title }}

+
    + {{ range .items }}
  • {{ . | markdownify }}
  • {{ end }} +
+
+ {{ end }} +
+{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/stats.html b/layouts/partials/sections/stats.html new file mode 100644 index 0000000..819d7b0 --- /dev/null +++ b/layouts/partials/sections/stats.html @@ -0,0 +1,45 @@ +{{/* + Stat tiles. Values that start with a number ("80%+", "100+", "30X") count + up from zero when the row scrolls into view; others show as written. + Without JS or with reduced motion, every value simply shows. +*/}} +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
+ {{ range $s.items }} +
+ {{ .value }} + {{ .label | markdownify }} +
+ {{ end }} +
+ +{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/steps.html b/layouts/partials/sections/steps.html new file mode 100644 index 0000000..c9e2e8d --- /dev/null +++ b/layouts/partials/sections/steps.html @@ -0,0 +1,14 @@ +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
    + {{ range $i, $item := $s.items }} +
  1. +
    +

    {{ $item.title }}

    + {{ printf "%03d" (add $i 1) }} +
    +

    {{ $item.body | markdownify }}

    +
  2. + {{ end }} +
+{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/table.html b/layouts/partials/sections/table.html new file mode 100644 index 0000000..db23503 --- /dev/null +++ b/layouts/partials/sections/table.html @@ -0,0 +1,23 @@ +{{/* + A plain table: `headers` (a list) and `rows` (a list of lists). The first + cell of each row is its label. Cells take Markdown. +*/}} +{{ $s := . }} +{{ partial "sections/head.html" $s }} +
+ + + {{ range $s.headers }}{{ end }} + + + {{ range $s.rows }} + + {{ range $i, $cell := . }} + {{ if eq $i 0 }}{{ else }}{{ end }} + {{ end }} + + {{ end }} + +
{{ . }}
{{ $cell | markdownify }}{{ $cell | markdownify }}
+
+{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} diff --git a/layouts/partials/sections/text.html b/layouts/partials/sections/text.html new file mode 100644 index 0000000..8624d78 --- /dev/null +++ b/layouts/partials/sections/text.html @@ -0,0 +1,20 @@ +{{/* + Prose, with optional `ctas`. `offer` adds a highlighted card below: + `badge` (a small cyan tag), `text`, and `ctas`. +*/}} +{{ $s := . }} +{{ partial "sections/head.html" $s }} +{{/* markdownify drops the

around a lone paragraph; put it back so inline markup stays in the flow */}} +{{ $body := $s.body | markdownify }} +{{ if not (findRE `^\s*<(p|ul|ol|h[1-6]|div|blockquote|pre|table|figure|hr)[\s>]` $body) }}{{ $body = printf "

%s

" $body | safeHTML }}{{ end }} +
+ {{ $body }} +
+{{ if $s.ctas }}
{{ partial "sections/ctas.html" $s.ctas }}
{{ end }} +{{ with $s.offer }} +
+ {{ with .badge }}{{ . }}{{ end }} +

{{ .text | markdownify }}

+ {{ with .ctas }}
{{ partial "sections/ctas.html" . }}
{{ end }} +
+{{ end }} diff --git a/layouts/partials/sections/wrapper.html b/layouts/partials/sections/wrapper.html new file mode 100644 index 0000000..ea65d72 --- /dev/null +++ b/layouts/partials/sections/wrapper.html @@ -0,0 +1,7 @@ +{{ $s := . }} +{{ $id := partial "section-id.html" $s }} +
+
+ {{ partial (printf "sections/%s.html" $s.type) $s }} +
+
diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..11559ae Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..b270a6f --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,900 @@ +/* ========================================================================== + Federated CoreDesktop — design system + Shares the Federated Computer look: a dark navy top bar and hero, + full-bleed bands that alternate white and light grey, a centered 1280px + container, big tight Inter headlines, rounded filled cards, a cyan + primary button and a magenta accent. Terminal windows carry the commands. + ========================================================================== */ + +:root { + /* color */ + --navy-deep: #001a2b; + --navy: #052b42; + --navy-2: #0f2839; + --cyan: #2ee6d6; + --cyan-deep: #14cfc0; + --magenta: #d4008a; + --magenta-deep: #a8006d; + --magenta-bright: #ff3daf; + --magenta-7: #d4008a12; + --magenta-11: #d4008a1c; + --paper: #f3f4f5; + --paper-2: #e9ebee; + --rule: #d9dee2; + --ink: #001a2b; + --ink-86: #001a2bdb; + --ink-68: #001a2bad; + --ink-60: #001a2b99; + --ink-45: #001a2b73; + --ink-22: #001a2b38; + --on-dark: #ffffff; + --on-dark-72: #ffffffb8; + --on-dark-56: #ffffff8f; + --on-dark-16: #ffffff29; + --on-dark-8: #ffffff14; + --live: #2ee6d6; + + /* type */ + --font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + --fs-statement: 64px; + --fs-h2: 48px; + --fs-dek: 20px; + --fs-body: 17px; + --lh-body: 1.6; + + /* layout */ + --wrap: 1280px; + --gutter: 32px; + --head-h: 72px; + --sp-section: 104px; + --radius: 16px; + --radius-sm: 10px; + --measure: 680px; + + /* motion */ + --ease-out: cubic-bezier(.22, 1, .36, 1); + --ease-hover: cubic-bezier(.25, .1, .25, 1); + --ease-standard: cubic-bezier(.4, 0, .2, 1); + --dur-hover: .2s; + --dur-press: .12s; +} + +@media (max-width: 1000px) { + :root { --fs-statement: 48px; --fs-h2: 38px; --sp-section: 80px; --head-h: 64px; } +} +@media (max-width: 600px) { + :root { --fs-statement: 38px; --fs-h2: 30px; --fs-dek: 17px; --fs-body: 16px; --sp-section: 64px; --gutter: 20px; } +} + +/* ---- utilities ---- */ +.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +.mb-2 { margin-bottom: 28px; } +.mt-1 { margin-top: 12px; } +.mt-2 { margin-top: 32px; } + +/* ---- reset ---- */ +*, *::before, *::after { box-sizing: border-box; } +html { + scroll-behavior: smooth; + scroll-padding-top: var(--head-h); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +body { + margin: 0; + background: #fff; + color: var(--ink); + font-family: var(--font); + font-size: var(--fs-body); + line-height: var(--lh-body); + font-feature-settings: "cv11", "ss01"; + display: flex; flex-direction: column; min-height: 100vh; +} +main { flex: 1 0 auto; } +img { max-width: 100%; display: block; } +a { color: inherit; } +h1, h2, h3, h4, p, figure, blockquote { margin: 0; } +h1, h2, h3 { text-wrap: balance; font-weight: 500; } +p, figcaption, dd { text-wrap: pretty; } +ol, ul { margin: 0; padding: 0; list-style: none; } +strong, b { font-weight: 600; } +main:focus { outline: none; } + +:focus-visible { outline: 2px solid var(--magenta); outline-offset: 3px; } + +.skip { + position: absolute; left: 12px; top: -48px; z-index: 50; + background: var(--cyan); color: var(--navy-deep); padding: 8px 12px; font-size: 14px; text-decoration: none; border-radius: 6px; +} +.skip:focus { top: 12px; } + +.wrap { width: min(var(--wrap), 100% - var(--gutter) * 2); margin-inline: auto; } + +.eyebrow, +.kicker { + font-family: var(--font-mono); + font-size: 12px; + font-weight: 500; + line-height: 1.3; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--magenta); +} + +/* ========================================================================== + Top bar — logo, pages, and the source / install actions + ========================================================================== */ +.site-head { + position: sticky; top: 0; z-index: 30; + background: var(--navy-deep); + color: var(--on-dark); + border-bottom: 1px solid var(--on-dark-8); + transition: background-color var(--dur-hover) var(--ease-hover); +} +.site-head.is-scrolled { background: #001a2bf2; backdrop-filter: blur(12px); } +.head-bar { display: flex; align-items: center; gap: 32px; height: var(--head-h); } +.brand { display: inline-flex; flex: none; } +.brand-logo { width: 168px; height: auto; translate: 0 -3px; } /* lifts the wordmark baseline onto the nav text baseline */ + +.head-nav { display: flex; align-items: center; gap: 2px; margin-right: auto; } +.nav-item { position: relative; } +.nav-link { + display: inline-flex; align-items: center; gap: 7px; + padding: 8px 12px; border-radius: 8px; + font-size: 15px; font-weight: 500; line-height: 1.3; + color: var(--on-dark-72); text-decoration: none; + transition: color var(--dur-hover) var(--ease-hover), background-color var(--dur-hover) var(--ease-hover); +} +.nav-link:hover, .nav-item:focus-within > .nav-link, .nav-link[aria-current] { color: var(--on-dark); } +.nav-link:hover { background: var(--on-dark-8); } +.head-actions { display: flex; align-items: center; gap: 20px; flex: none; } +.head-signin { + font-size: 15px; font-weight: 500; color: var(--on-dark-72); text-decoration: none; + transition: color var(--dur-hover) var(--ease-hover); +} +.head-signin:hover, .head-signin[aria-current] { color: var(--on-dark); } + +/* ---- mobile menu (a
, so it works with JS off) ---- */ +.menu { display: none; margin-left: auto; } +.menu summary { + list-style: none; + display: inline-flex; align-items: center; gap: 10px; + height: 40px; padding: 0 14px; cursor: pointer; + border: 1px solid var(--on-dark-16); border-radius: 8px; + font-size: 14px; font-weight: 500; color: var(--on-dark); +} +.menu summary::-webkit-details-marker { display: none; } +.menu-glyph { display: inline-grid; gap: 4px; width: 14px; } +.menu-glyph i { display: block; height: 1.5px; background: currentColor; transition: rotate .2s var(--ease-out), translate .2s var(--ease-out); } +.menu[open] .menu-glyph i { translate: 0 2.75px; rotate: 45deg; } +.menu[open] .menu-glyph i + i { translate: 0 -2.75px; rotate: -45deg; } +.menu-panel { + position: absolute; left: 0; right: 0; top: 100%; + max-height: calc(100vh - var(--head-h)); overflow-y: auto; + background: #fff; color: var(--ink); + border-bottom: 1px solid var(--rule); + box-shadow: 0 24px 40px -20px #001a2b40; + display: flex; flex-direction: column; + padding: 8px var(--gutter) 24px; +} +.menu-panel a { text-decoration: none; } +.menu-top { padding: 14px 0; font-size: 16px; font-weight: 600; border-top: 1px solid var(--rule); } +.menu-top:first-child { border-top: 0; } +.menu-sub { display: flex; flex-direction: column; padding: 0 0 12px 14px; margin-top: -6px; } +.menu-sub a { padding: 7px 0; font-size: 15px; color: var(--ink-68); } +.menu-actions { display: flex; flex-wrap: wrap; gap: 12px; padding-top: 18px; border-top: 1px solid var(--rule); } + +@media (max-width: 1080px) { + .head-nav, .head-actions { display: none; } + .menu { display: block; } + .brand-logo { width: 144px; } +} + +/* ========================================================================== + Buttons & links + ========================================================================== */ +.btn { + position: relative; + display: inline-flex; align-items: center; justify-content: center; gap: 10px; + height: 48px; padding: 0 22px; + font-family: var(--font); + font-size: 15px; font-weight: 600; line-height: 1; + white-space: nowrap; text-decoration: none; + border: 1px solid transparent; border-radius: 8px; cursor: pointer; + transition: background-color var(--dur-hover) var(--ease-hover), border-color var(--dur-hover) var(--ease-hover), color var(--dur-hover) var(--ease-hover), transform var(--dur-press) var(--ease-out); +} +.btn:active { transform: scale(.98); } +.btn .arrow { margin: 0; } + +.btn-primary { background: var(--cyan); color: var(--navy-deep); } +.btn-primary:hover, .btn-primary:focus-visible { background: var(--cyan-deep); } + +.btn-secondary { background: transparent; color: var(--ink); border-color: var(--ink-22); } +.btn-secondary:hover { border-color: var(--ink); } + +.btn-sm { height: 40px; padding: 0 16px; font-size: 14px; } + +/* on the dark bands */ +.hero .btn-secondary, .cta .btn-secondary, .section-stats .btn-secondary { + color: var(--on-dark); border-color: var(--on-dark-16); +} +.hero .btn-secondary:hover, .cta .btn-secondary:hover, .section-stats .btn-secondary:hover { + background: var(--on-dark-8); border-color: var(--on-dark-56); +} +/* on white, the cyan needs a navy edge to hold its shape */ +.section .btn-primary { background: var(--navy-deep); color: #fff; } +.section .btn-primary:hover { background: var(--magenta); } + +/* the quiet link */ +.more, .btn-text { + display: inline-flex; align-items: center; + font-size: 15px; font-weight: 600; line-height: 1.4; + color: var(--magenta); text-decoration: none; + transition: color var(--dur-hover) var(--ease-hover); +} +.more:hover, .btn-text:hover { color: var(--magenta-deep); } +.arrow { + display: inline-block; margin-inline-start: .4em; font-weight: 400; + transition: translate var(--dur-hover) var(--ease-hover); +} +a:hover > .arrow, a:focus-visible > .arrow, a:hover .arrow { translate: 3px 0; } + +.actions, .cta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } +.cta-row { margin-top: 40px; gap: 12px 32px; } +@media (max-width: 520px) { + .hero .actions, .cta .actions { flex-direction: column; align-items: stretch; } +} + +/* ========================================================================== + Hero — a full-bleed navy band, copy left, a visual right + ========================================================================== */ +.hero { + position: relative; isolation: isolate; overflow: hidden; + padding: 104px 0 120px; + background: + radial-gradient(60% 80% at 92% 8%, #d4008a66 0%, transparent 60%), + radial-gradient(40% 50% at 70% 110%, #2ee6d626 0%, transparent 70%), + var(--navy-deep); + color: var(--on-dark); +} +.hero::before { + content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; + background: + linear-gradient(var(--on-dark-8) 1px, transparent 1px) 0 0 / 64px 64px, + linear-gradient(90deg, var(--on-dark-8) 1px, transparent 1px) 0 0 / 64px 64px; + -webkit-mask-image: radial-gradient(80% 90% at 80% 30%, #000 0%, transparent 75%); + mask-image: radial-gradient(80% 90% at 80% 30%, #000 0%, transparent 75%); + opacity: .7; +} +.hero-compact { padding: 80px 0 88px; } +.hero-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 56px; align-items: center; } +.hero.has-visual .hero-grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 64px; } +.hero-copy { max-width: 820px; } + + +.note { + display: inline-flex; align-items: center; gap: 10px; + padding: 6px 14px 6px 12px; + border: 1px solid var(--on-dark-16); border-radius: 999px; + background: var(--on-dark-8); + font-size: 14px; line-height: 1.45; color: var(--on-dark-72); +} +.note-live { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 0 4px #2ee6d626; } + +.statement { + font-size: var(--fs-statement); + line-height: 1.04; + letter-spacing: -.035em; + font-weight: 500; + color: var(--on-dark); +} +.note + .statement { margin-top: 28px; } +.dek { + margin-top: 24px; + max-width: 36em; + font-size: var(--fs-dek); + line-height: 1.55; + color: var(--on-dark-72); +} +.dek strong { color: var(--on-dark); font-weight: 600; } +.hero .actions { margin-top: 40px; } +.hero-compact .statement { font-size: calc(var(--fs-statement) * .85); } +.hero-compact:not(.has-visual) .hero-copy { max-width: 900px; } + +@media (max-width: 1000px) { + .hero, .hero-compact { padding: 72px 0 80px; } + .hero.has-visual .hero-grid { grid-template-columns: minmax(0, 1fr); gap: 48px; } +} + +/* the agent log: a glass console, typed out by the hero's script */ +.hero-log { position: relative; } +.log-window { + background: #001a2bb8; + -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); + border-radius: var(--radius); + box-shadow: inset 0 0 0 1px var(--on-dark-16), 0 40px 80px -24px #000000a6; + overflow: hidden; +} +.log-bar { + display: flex; justify-content: space-between; align-items: center; gap: 12px; + padding: 12px 18px; border-bottom: 1px solid var(--on-dark-8); background: var(--on-dark-8); + font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .02em; color: var(--on-dark-56); +} +.log-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.log-live { display: inline-flex; align-items: center; gap: 8px; flex: none; text-transform: uppercase; letter-spacing: .08em; font-weight: 500; color: var(--cyan); } +.log-live .note-live { box-shadow: none; width: 6px; height: 6px; } +.log-lines { position: relative; display: flex; flex-direction: column; gap: 8px; padding: 18px 18px 22px; font-family: var(--font-mono); font-size: 13px; line-height: 1.5; } +.log-line { display: grid; grid-template-columns: 80px minmax(0, 1fr); gap: 12px; align-items: baseline; } +.log-tag { font-size: 10.5px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--magenta-bright); } +.log-text { color: #ffffffe0; } +.log-line.is-done .log-tag { color: var(--cyan); } +.log-line.is-done .log-text { color: var(--cyan); font-weight: 500; } +/* animated only with JS and motion allowed; otherwise every line simply shows */ +.hero-log.is-animated .log-line { opacity: 0; } +.hero-log.is-animated .log-line.is-on { opacity: 1; } +.hero-log.is-animated .log-line.is-on .log-text { animation: log-type calc(var(--n) * 16ms) steps(var(--n)) both; } +.hero-log.is-animated .log-line.is-typed .log-text { animation: none; } +/* a fixed-height window that scrolls as lines arrive, fading out at the top */ +.hero-log.is-animated .log-view { + height: 300px; overflow: hidden; + -webkit-mask-image: linear-gradient(to bottom, transparent, #000 32px); + mask-image: linear-gradient(to bottom, transparent, #000 32px); +} +.hero-log.is-animated .log-lines { transition: transform .45s var(--ease-out), opacity .4s var(--ease-standard); } +/* reasoning steps: greyed, with a spinner in place of the cursor */ +.log-line.is-think .log-tag { color: var(--on-dark-56); } +.log-line.is-think .log-text { color: var(--on-dark-56); font-style: italic; } +.hero-log.is-animated .log-line.is-current .log-text::after { + content: ""; display: inline-block; width: .6em; height: 1.1em; margin-left: 2px; vertical-align: text-bottom; + background: var(--cyan); animation: log-caret 1s steps(1) infinite; +} +.hero-log.is-animated .log-line.is-think.is-current .log-text::after { + content: "⠋"; width: auto; height: auto; margin-left: 6px; background: none; + font-style: normal; color: var(--cyan); animation: log-spin .8s steps(1) infinite; +} +@keyframes log-spin { + 0% { content: "⠋"; } 12.5% { content: "⠙"; } 25% { content: "⠹"; } 37.5% { content: "⠸"; } + 50% { content: "⠼"; } 62.5% { content: "⠴"; } 75% { content: "⠦"; } 87.5% { content: "⠧"; } +} +.hero-log.is-clearing .log-lines { opacity: 0; } +@keyframes log-type { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } } +@keyframes log-caret { 50% { opacity: 0; } } +@media (max-width: 600px) { + .log-lines { font-size: 12px; } + .log-line { grid-template-columns: minmax(0, 1fr); gap: 0; } + .hero-log.is-animated .log-view { height: 380px; } +} +/* ========================================================================== + Sections — full-bleed bands that alternate white and grey + ========================================================================== */ +.section { + --section-bg: #fff; + --surface: var(--paper); + padding-block: var(--sp-section); + background: var(--section-bg); +} +main > .section:nth-of-type(even) { --section-bg: var(--paper); --surface: #fff; } + +.section-head { margin-bottom: 56px; max-width: 880px; } +.section-head .eyebrow { margin-bottom: 18px; } +.section-head h2 { + font-size: var(--fs-h2); line-height: 1.08; letter-spacing: -.03em; color: var(--ink); +} +.section-head .lede { + margin-top: 20px; max-width: var(--measure); + font-size: 19px; line-height: 1.55; color: var(--ink-68); +} +.section-head:has(.eyebrow):not(:has(h2)) { margin-bottom: 20px; } + +/* text sections: the heading on the left, the copy on the right */ +@media (min-width: 1001px) { + .section-text .wrap:has(> .section-head) { + display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); column-gap: 80px; align-items: start; + } + .section-text .wrap > .section-head { grid-row: span 3; margin-bottom: 0; } + .section-text .wrap > .prose, .section-text .wrap > .cta-row { grid-column: 2; } + .section-text .wrap > .prose { padding-top: 34px; } +} +/* a text section without a heading is a large statement */ +.section-text .wrap:not(:has(> .section-head)) .prose { max-width: 1040px; } +.section-text .wrap:not(:has(> .section-head)) .prose, +.section-text .wrap:not(:has(> .section-head)) .prose p { + font-size: clamp(24px, 3vw, 36px); line-height: 1.3; letter-spacing: -.02em; font-weight: 500; color: var(--ink); +} +/* *phrases* in a statement are picked out in magenta */ +.section-text .wrap:not(:has(> .section-head)) .prose em { font-weight: inherit; color: var(--magenta); } + +/* an offer card under a text section */ +.offer-card { + display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 24px; + max-width: 1040px; margin-top: 48px; padding: 24px 28px; + border: 1px solid var(--magenta-11); border-radius: var(--radius); + background: var(--magenta-7); +} +.offer-badge { + padding: 6px 12px; border-radius: 999px; + background: var(--cyan); color: var(--navy-deep); + font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; +} +.offer-text { font-size: 20px; line-height: 1.4; font-weight: 500; color: var(--ink); } +.offer-text a { color: var(--magenta); text-decoration: none; } +.offer-text a:hover { color: var(--magenta-deep); text-decoration: underline; } +@media (max-width: 700px) { + .offer-card { grid-template-columns: minmax(0, 1fr); justify-items: start; gap: 16px; padding: 22px; } +} + +.rule-dashed { border: 0; height: 1px; margin: 0; background: var(--rule); } +.section-divider { padding: 0; } + +/* ---- prose ---- */ +.prose { display: flex; flex-direction: column; gap: 22px; max-width: var(--measure); } +.prose.centered { margin-inline: auto; text-align: center; } +.prose p, .prose li { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-86); } +.prose em { font-style: normal; font-weight: 600; } +.prose a { color: var(--magenta); text-decoration: none; font-weight: 500; } +.prose a:hover { color: var(--magenta-deep); text-decoration: underline; } +.prose ul { display: flex; flex-direction: column; gap: 10px; } +.prose ul li { position: relative; padding-left: 22px; } +.prose ul li::before { content: ""; position: absolute; left: 2px; top: .62em; width: 7px; height: 7px; border-radius: 50%; background: var(--magenta); } +.prose h3 { font-size: 19px; font-weight: 600; } +/* ---- generic cards ---- */ +.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; } +.card-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.card-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +@media (max-width: 800px) { .card-grid.cols-2, .card-grid.cols-3 { grid-template-columns: minmax(0, 1fr); } } +.card { + display: flex; flex-direction: column; + border-radius: var(--radius); overflow: hidden; + background: var(--surface); +} +.card-visual { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; } +.card-body { display: flex; flex-direction: column; flex: 1; gap: 12px; padding: 32px 28px 30px; min-height: 240px; } +.card.has-visual .card-body { min-height: 0; } +.card-body::before { + content: ""; width: 40px; height: 40px; margin-bottom: 16px; border-radius: 10px; + background: + radial-gradient(circle at 50% 50%, var(--magenta) 0 5px, transparent 5.5px), + var(--magenta-11); +} +.card.has-visual .card-body::before { display: none; } +.card-label { font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--magenta); } +.card-title { font-size: 22px; line-height: 1.25; letter-spacing: -.015em; } +.card-title p { margin: 0; } +.card-text { font-size: 15px; line-height: 1.6; color: var(--ink-68); } +.card-link { margin-top: auto; padding-top: 8px; } + +/* an abstract gradient panel used where there's no image yet */ +.card-art { + position: relative; display: block; overflow: hidden; + background: + radial-gradient(#ffffff1f 1px, transparent 1.25px) 8px 8px / 16px 16px, + radial-gradient(70% 80% at calc(15% + var(--i, 0) * 17%) 20%, #d4008acc 0%, transparent 60%), + radial-gradient(60% 70% at calc(90% - var(--i, 0) * 13%) 100%, #2ee6d68c 0%, transparent 65%), + var(--navy); +} +/* ---- stats: a navy band with cyan figures (:nth-of-type(n) outranks the even-row paper tint) ---- */ +main > .section-stats:nth-of-type(n) { --section-bg: var(--navy-deep); --surface: var(--on-dark-8); color: var(--on-dark); } +.section-stats .section-head h2 { color: var(--on-dark); } +.section-stats .section-head .lede { color: var(--on-dark-72); } +.section-stats .eyebrow { color: var(--cyan); } +.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 32px 0; } +.stat { + display: flex; flex-direction: column; gap: 16px; + padding: 4px 28px 8px; + border-left: 1px solid var(--on-dark-16); +} +.stat-value { + font-size: clamp(40px, 4.4vw, 60px); font-weight: 500; line-height: 1; letter-spacing: -.04em; color: var(--cyan); + font-variant-numeric: tabular-nums; +} +.stat-label { font-size: 16px; line-height: 1.5; color: var(--on-dark-72); max-width: 18em; } + +/* ---- quote ---- */ +.said, .pull-quote { border-left: 3px solid var(--magenta); padding: 4px 0 4px 28px; } +.said blockquote, .said blockquote p, .pull-quote { + font-size: 28px; line-height: 1.4; letter-spacing: -.02em; font-weight: 500; color: var(--ink); max-width: 34em; +} +.said figcaption { margin-top: 16px; font-size: 15px; color: var(--ink-60); } +.said-list { display: flex; flex-direction: column; gap: 44px; } +.said-list .said blockquote, .said-list .said blockquote p { font-size: 20px; line-height: 1.5; } +.said-list .said:only-child blockquote, .said-list .said:only-child blockquote p { font-size: 28px; line-height: 1.4; } +.said-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.said-card { + display: flex; flex-direction: column; justify-content: space-between; gap: 28px; + padding: 32px 28px 28px; border-radius: var(--radius); background: var(--surface); +} +.said-card blockquote, .said-card blockquote p { margin: 0; font-size: 17px; line-height: 1.6; color: var(--ink-86); } +.said-card blockquote::before { content: "\201C"; } +.said-card blockquote::after { content: "\201D"; } +.said-who { display: flex; align-items: center; gap: 14px; } +.said-photo { flex: none; width: 48px; height: 48px; border-radius: 50%; object-fit: cover; background: var(--magenta-11); } +.said-initials { display: grid; place-items: center; font-size: 15px; font-weight: 600; color: var(--magenta); } +.said-id { display: flex; flex-direction: column; min-width: 0; font-size: 14px; line-height: 1.4; } +.said-name { font-weight: 600; color: var(--ink); } +.said-role, .said-role p { margin: 0; color: var(--ink-60); } +.said-role a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--rule); } +.said-role a:hover { color: var(--magenta); border-color: currentColor; } +@media (max-width: 1000px) { .said-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 640px) { .said-cards { grid-template-columns: minmax(0, 1fr); } } +.said-savings { margin-top: 64px; } +.said-savings-title { margin-bottom: 20px; font-size: 28px; line-height: 1.25; letter-spacing: -.02em; font-weight: 500; } + +/* ---- tables ---- */ +.table-scroll { overflow-x: auto; border-radius: var(--radius); background: #fff; box-shadow: inset 0 0 0 1px var(--rule); } +.compare-table { width: 100%; border-collapse: collapse; font-size: 15px; line-height: 1.5; } +.compare-table th, .compare-table td { padding: 14px 20px; text-align: left; vertical-align: top; border-top: 1px solid var(--rule); } +.compare-table thead th { + border-top: 0; background: var(--navy-deep); color: var(--on-dark-72); + font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; +} +.compare-table thead th a { text-decoration: none; color: var(--on-dark); } +.compare-table thead th a:hover { color: var(--cyan); } +.compare-table tbody th { font-weight: 600; color: var(--ink); } +.compare-table tbody tr:hover { background: #f8f9fa; } +.compare-table td.bad { color: var(--ink-60); } +.compare-table td.good { color: var(--ink); font-weight: 600; } +.compare-table td.good::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin: 0 8px 1px 0; background: var(--magenta); } +/* ---- checklist & features ---- */ +.check-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px 40px; } +.check-list li { position: relative; padding-left: 34px; font-size: 16px; line-height: 1.55; color: var(--ink-86); } +.check-list li::before { + content: ""; position: absolute; left: 0; top: .15em; + width: 22px; height: 22px; border-radius: 50%; + background: var(--magenta) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8.5l2.5 2.5L12 5.5' fill='none' stroke='%23fff' stroke-width='2'/%3E%3C/svg%3E") center / 14px no-repeat; +} +.check-list li p { margin: 0; } + +.feature-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; } +.feature-list li { + display: flex; flex-direction: column; gap: 10px; + padding: 30px 28px; border-radius: var(--radius); background: var(--surface); + font-size: 15px; line-height: 1.6; color: var(--ink-68); +} +.feature-list li::before { + content: ""; width: 36px; height: 4px; margin-bottom: 12px; border-radius: 99px; background: var(--magenta); +} +.feature-list li strong { display: block; font-size: 20px; font-weight: 500; letter-spacing: -.015em; line-height: 1.3; color: var(--ink); } +.feature-list li p { margin: 0; } + +/* ---- steps ---- */ +.step-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; counter-reset: step; } +.step-row.steps-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.step { display: flex; flex-direction: column; gap: 14px; padding: 28px; border-radius: var(--radius); background: var(--surface); } +.step-head { display: flex; flex-direction: column-reverse; gap: 20px; } +.step h3 { font-size: 20px; font-weight: 500; letter-spacing: -.015em; line-height: 1.3; } +.step-num { + display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; + background: var(--navy-deep); color: var(--cyan); + font-family: var(--font-mono); font-size: 13px; font-weight: 500; +} +.step p { font-size: 15px; line-height: 1.6; color: var(--ink-68); } +@media (max-width: 1000px) { .step-row.steps-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 700px) { .step-row, .step-row.steps-4 { grid-template-columns: minmax(0, 1fr); } } + +/* ---- faq & app directory (native
) ---- */ +.faq-list { max-width: 960px; border-top: 1px solid var(--rule); } +.faq-item { border-bottom: 1px solid var(--rule); } +.faq-item summary { + list-style: none; cursor: pointer; + display: flex; align-items: center; justify-content: space-between; gap: 24px; + padding: 24px 0; + font-size: 19px; font-weight: 500; letter-spacing: -.01em; line-height: 1.4; + transition: color var(--dur-hover) var(--ease-hover); +} +.faq-item summary:hover { color: var(--magenta); } +.faq-item summary::-webkit-details-marker { display: none; } +.faq-item summary::after { + content: ""; flex: none; width: 32px; height: 32px; border-radius: 50%; + background: + linear-gradient(currentColor, currentColor) center / 12px 1.5px no-repeat, + linear-gradient(currentColor, currentColor) center / 1.5px 12px no-repeat, + var(--surface); + transition: rotate .25s var(--ease-out); +} +.faq-item[open] summary::after { rotate: 45deg; } +.faq-answer { padding: 0 56px 26px 0; max-width: 820px; } +.faq-answer p { font-size: 16px; line-height: 1.65; color: var(--ink-68); } +/* ---- callout ---- */ +.callout { max-width: 960px; padding: 28px 32px; border-radius: var(--radius); background: var(--magenta-7); box-shadow: inset 4px 0 0 var(--magenta); } +.callout p { font-size: 17px; line-height: 1.65; color: var(--ink-86); } +/* ---- split ---- */ +.split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } +.split-col { padding: 32px; border-radius: var(--radius); background: var(--surface); } +.split-col:first-child { background: var(--navy-deep); color: var(--on-dark); } +.split-col h3 { margin-bottom: 20px; font-family: var(--font-mono); font-size: 12px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--magenta); } +.split-col:first-child h3 { color: var(--cyan); } +.split-list { display: flex; flex-direction: column; gap: 12px; } +.split-list li { position: relative; padding-left: 22px; font-size: 16px; line-height: 1.5; color: var(--ink-86); } +.split-col:first-child .split-list li { color: #ffffffe0; } +.split-list li::before { content: ""; position: absolute; left: 0; top: .55em; width: 8px; height: 8px; border-radius: 50%; background: var(--cyan); } +.split-col + .split-col .split-list li::before { background: transparent; border: 1.5px solid var(--ink-45); } +.split-list li p { margin: 0; } +@media (max-width: 700px) { .split { grid-template-columns: minmax(0, 1fr); } } +/* ========================================================================== + Closing call to action — a navy band + ========================================================================== */ +.cta { + position: relative; isolation: isolate; overflow: hidden; + padding: calc(var(--sp-section) * 1.1) 0; + color: var(--on-dark); + background: + radial-gradient(50% 120% at 100% 100%, #d4008a8c 0%, transparent 60%), + radial-gradient(30% 60% at 0% 0%, #2ee6d61f 0%, transparent 70%), + var(--navy-deep); +} +.cta::before { + content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; + background: + linear-gradient(var(--on-dark-8) 1px, transparent 1px) 0 0 / 64px 64px, + linear-gradient(90deg, var(--on-dark-8) 1px, transparent 1px) 0 0 / 64px 64px; + -webkit-mask-image: radial-gradient(60% 100% at 85% 60%, #000 0%, transparent 75%); + mask-image: radial-gradient(60% 100% at 85% 60%, #000 0%, transparent 75%); +} +.cta-inner { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 40px 64px; } +.cta-title { max-width: 16em; font-size: var(--fs-h2); line-height: 1.08; letter-spacing: -.03em; color: var(--on-dark); } +.cta-text { margin-top: 20px; max-width: 36em; font-size: 18px; line-height: 1.6; color: var(--on-dark-72); } +@media (max-width: 900px) { .cta-inner { grid-template-columns: minmax(0, 1fr); } } +/* ========================================================================== + Footer — navy, many columns + ========================================================================== */ +.foot { + padding: 88px 0 36px; + background: var(--navy-deep); + font-size: 14px; line-height: 1.5; color: var(--on-dark-56); + border-top: 1px solid var(--on-dark-8); +} +.foot a { color: var(--on-dark-72); text-decoration: none; transition: color var(--dur-hover) var(--ease-hover); } +.foot a:hover { color: var(--on-dark); } +.foot-top-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 56px 80px; } +.foot-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; } +.foot-mark .brand-logo { width: 200px; } +.foot-line { max-width: 32ch; font-size: 15px; color: var(--on-dark-72); } +.foot-place { font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .08em; text-transform: uppercase; } +.foot-head { font-size: 14px; font-weight: 600; color: var(--on-dark); } +.foot-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 40px 28px; } +.foot-group { display: flex; flex-direction: column; gap: 18px; min-width: 0; } +.foot-links { display: flex; flex-direction: column; gap: 12px; } +.foot-legal { + display: flex; align-items: center; gap: 24px; + margin-top: 72px; padding-top: 24px; + border-top: 1px solid var(--on-dark-8); + font-size: 13px; +} +.foot-social { display: flex; gap: 10px; margin-top: 8px; } +.foot-social a { display: inline-flex; padding: 7px 14px; border-radius: 999px; border: 1px solid var(--on-dark-16); font-size: 13px; } +.foot-social a:hover { border-color: var(--on-dark-56); } +.foot-social .arrow { margin-left: 4px; } +.foot-policy { white-space: nowrap; } +.foot-top { margin-left: auto; white-space: nowrap; display: inline-flex; gap: 6px; } +.foot-up { display: inline-block; transition: translate var(--dur-hover) var(--ease-hover); } +.foot-top:hover .foot-up { translate: 0 -2px; } +@media (max-width: 1000px) { .foot-top-row { grid-template-columns: minmax(0, 1fr); } } +@media (max-width: 600px) { + .foot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .foot-legal { flex-wrap: wrap; row-gap: 10px; } + .foot-top { margin-left: 0; } +} +/* ========================================================================== + Reveal — content rises in as it scrolls into view (JS adds html.js) + ========================================================================== */ +html.js [data-reveal] { + opacity: 0; + translate: 0 16px; + transition: opacity .7s var(--ease-standard), translate .7s var(--ease-out); + transition-delay: var(--d, 0s); +} +html.js [data-reveal].is-visible { opacity: 1; translate: 0; } +/* the first screen paints at once — never hidden behind an animation */ +html.js .hero [data-reveal] { opacity: 1; translate: 0; transition: none; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + html.js [data-reveal] { opacity: 1; translate: 0; transition: none; } + *, *::before, *::after { transition-duration: .001ms !important; } +} + +/* ========================================================================== + CoreDesktop additions — the wordmark, the tiled-desktop visual, terminal + windows, and code in prose + ========================================================================== */ + +.head-signin .arrow { font-size: 13px; } + +/* the tiles: a three-pane GNOME desktop drawn on a deep panel */ +.hero-tiles { + position: relative; display: flex; flex-direction: column; gap: 12px; + aspect-ratio: 1.35; padding: 14px; + overflow: hidden; border-radius: 20px; + background: + radial-gradient(#ffffff1f 1px, transparent 1.25px) 10px 10px / 20px 20px, + radial-gradient(90% 90% at 90% 0%, #d4008a99 0%, transparent 60%), + var(--navy); + box-shadow: inset 0 0 0 1px var(--on-dark-16); +} +.hero-compact .hero-tiles { aspect-ratio: 1.6; } +.tiles-bar { + display: flex; align-items: center; justify-content: space-between; gap: 12px; + padding: 0 6px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .06em; color: var(--on-dark-72); +} +.tiles-clock { font-weight: 500; color: var(--on-dark); } +.tiles-grid-icon { display: inline-grid; grid-template-columns: repeat(3, 6px); gap: 2px; } +.tiles-grid-icon i { height: 9px; border-radius: 1.5px; background: var(--on-dark-56); } +.tiles-grid { flex: 1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; min-height: 0; } +.tile { + display: flex; flex-direction: column; gap: 9px; + padding: 14px; border-radius: 12px; + background: #001a2bcc; box-shadow: inset 0 0 0 1px var(--on-dark-16); + animation: tile-in .8s var(--ease-out) both; animation-delay: calc(.3s + var(--i) * .12s); +} +.tile.is-focus { box-shadow: inset 0 0 0 1.5px var(--cyan), 0 0 0 4px #2ee6d61f; } +.tile em { + margin-bottom: 6px; font-style: normal; font-family: var(--font-mono); + font-size: 10.5px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--on-dark-72); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.tile.is-focus em { color: var(--cyan); } +.tile i { display: block; height: 6px; border-radius: 99px; background: var(--on-dark-16); } +.tile i:nth-of-type(1) { width: 82%; } +.tile i:nth-of-type(2) { width: 64%; } +.tile i:nth-of-type(3) { width: 74%; } +.tile.is-focus i:nth-of-type(1) { background: var(--magenta); } +.tiles-keys { display: flex; align-items: center; gap: 6px; padding: 0 4px; font-size: 12px; color: var(--on-dark-56); } +.tiles-keys kbd, .prose kbd { + display: inline-block; padding: 2px 7px; border-radius: 5px; + font-family: var(--font-mono); font-size: 11px; line-height: 1.5; +} +.tiles-keys kbd { background: var(--on-dark-8); box-shadow: inset 0 -1px 0 var(--on-dark-16), inset 0 0 0 1px var(--on-dark-16); color: var(--on-dark); } +.tiles-hint { margin-left: 6px; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; } +@keyframes tile-in { from { opacity: 0; translate: 0 10px; } } +@media (max-width: 600px) { + .hero-tiles, .hero-compact .hero-tiles { aspect-ratio: 1.05; } + .tile { padding: 10px; } +} + +/* the window dots, shared by the hero log and terminal blocks */ +.log-bar { justify-content: flex-start; } +.log-bar .log-live { margin-left: auto; } +.log-dots { display: inline-flex; gap: 6px; flex: none; } +.log-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--on-dark-16); } +.log-dots i:first-child { background: var(--cyan); } + +/* a command in a terminal window */ +.term { + position: relative; max-width: 960px; + border-radius: var(--radius); overflow: hidden; + background: var(--navy-deep); color: var(--on-dark); + box-shadow: 0 30px 60px -30px #001a2b80; +} +.term-bar { + display: flex; align-items: center; gap: 14px; + padding: 12px 18px; border-bottom: 1px solid var(--on-dark-8); background: var(--on-dark-8); + font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .02em; color: var(--on-dark-56); +} +.term-body { + margin: 0; padding: 22px 24px 24px; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; + font-family: var(--font-mono); font-size: 14.5px; line-height: 1.6; color: #ffffffe0; +} +.term-body code::before { content: "$ "; color: var(--cyan); user-select: none; } +.term-note { margin-top: 18px; max-width: var(--measure); font-size: 15px; line-height: 1.6; color: var(--ink-68); } +.term-note code, .sib-desc code, .lede code, .card-text code, .feature-list code, .step code, .faq-answer code, .split-list code, .check-list code, .compare-table code, .callout code, .stat-label code, .dek code { + font-family: var(--font-mono); font-size: .88em; padding: .1em .38em; border-radius: 5px; background: var(--magenta-7); color: var(--ink); +} +.dek code { background: var(--on-dark-8); color: var(--on-dark); } + +/* the Copy button (added by baseof.html's script) */ +.copy-btn { + position: absolute; top: 8px; right: 10px; + padding: 4px 12px; border: 1px solid var(--on-dark-16); border-radius: 999px; + background: var(--navy-deep); color: var(--on-dark-72); + font: inherit; font-family: var(--font); font-size: 12px; font-weight: 500; cursor: pointer; + transition: color var(--dur-hover) var(--ease-hover), border-color var(--dur-hover) var(--ease-hover); +} +.copy-btn:hover { color: var(--on-dark); border-color: var(--on-dark-56); } +pre.has-copy { position: relative; } +pre.has-copy code { display: block; padding-right: 64px; } + +/* code in prose */ +.prose code { + font-family: var(--font-mono); font-size: .88em; + padding: .12em .4em; border-radius: 5px; + background: var(--magenta-7); color: var(--ink); +} +.prose pre { + margin: 0; padding: 18px 22px; overflow-x: auto; + border-radius: 12px; background: var(--navy-deep); + box-shadow: 0 20px 40px -28px #001a2b80; +} +.prose pre code { + padding: 0; background: none; color: #ffffffe0; + font-size: 14px; line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; +} +.prose kbd { font-size: .78em; background: #fff; box-shadow: inset 0 -1px 0 var(--rule), inset 0 0 0 1px var(--rule); color: var(--ink); } +.prose ol { display: flex; flex-direction: column; gap: 12px; counter-reset: li; } +.prose ol > li { position: relative; padding-left: 40px; counter-increment: li; } +.prose ol > li::before { + content: counter(li); position: absolute; left: 0; top: .1em; + display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; + background: var(--navy-deep); color: var(--cyan); + font-family: var(--font-mono); font-size: 12px; font-weight: 500; +} +.prose h3 + p, .prose h3 + ul, .prose h3 + pre { margin-top: -10px; } +.prose h3:not(:first-child) { margin-top: 10px; } +.prose strong { color: var(--ink); } +.prose blockquote { padding: 4px 0 4px 20px; border-left: 3px solid var(--magenta); } + +/* plain tables: a header row in navy, row labels in bold */ +.compare-table td { color: var(--ink-86); } +.compare-table td p, .compare-table th p { margin: 0; } +.compare-table tbody th { min-width: 140px; } +.compare-table a { color: var(--magenta); text-decoration: none; font-weight: 500; } + +.stat-label code { background: var(--on-dark-8); color: var(--on-dark); } + +/* links inside card, list and band copy */ +.card-text a, .feature-list a, .check-list a, .callout a, .step a, .faq-answer a, .split-list a, .term-note a, .lede a { + color: var(--magenta); text-decoration: none; font-weight: 500; +} +.card-text a:hover, .feature-list a:hover, .check-list a:hover, .callout a:hover, .step a:hover, .faq-answer a:hover, .split-list a:hover, .term-note a:hover, .lede a:hover { text-decoration: underline; } +.cta-text a, .dek a { color: var(--cyan); text-decoration: none; } +.cta-text a:hover, .dek a:hover { text-decoration: underline; } +.split-col:first-child .split-list code { background: var(--on-dark-8); color: var(--on-dark); } + +/* ---- sibling directory (data/siblings.yaml) ---- */ +.sib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; } +@media (max-width: 420px) { .sib-grid { grid-template-columns: minmax(0, 1fr); } } +.sib-card { + display: flex; flex-direction: column; gap: 14px; + padding: 28px; border-radius: var(--radius); background: var(--surface); + transition: box-shadow var(--dur-hover) var(--ease-hover); +} +.sib-card:hover { box-shadow: inset 0 0 0 1.5px var(--magenta); } +.sib-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; } +.sib-name { font-family: var(--font-mono); font-size: 18px; font-weight: 500; letter-spacing: -.01em; } +.sib-name a { color: var(--ink); text-decoration: none; } +.sib-name a:hover { color: var(--magenta); } +.sib-author { font-size: 14px; color: var(--ink-60); } +.sib-author a { color: inherit; } +.sib-desc { font-size: 15px; line-height: 1.6; color: var(--ink-68); } +.sib-tags { display: flex; flex-wrap: wrap; gap: 6px; } +.sib-tags li { + padding: 3px 10px; border-radius: 999px; background: var(--magenta-11); + font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; color: var(--magenta); +} +.sib-cmd { + margin: auto 0 0; padding: 14px 16px; border-radius: 10px; + background: var(--navy-deep); overflow-x: auto; + font-family: var(--font-mono); font-size: 13px; line-height: 1.5; color: #ffffffe0; + white-space: pre-wrap; overflow-wrap: anywhere; +} +.sib-cmd code::before { content: "$ "; color: var(--cyan); user-select: none; } +.sib-cmd .copy-btn { top: 9px; right: 9px; } + +.sib-empty { + display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 24px 28px; + max-width: 1040px; padding: 28px 32px; + border: 1.5px dashed var(--ink-22); border-radius: var(--radius); +} +.sib-empty-mark { + width: 48px; height: 48px; border-radius: 12px; + background: + linear-gradient(var(--magenta), var(--magenta)) center / 18px 3px no-repeat, + linear-gradient(var(--magenta), var(--magenta)) center / 3px 18px no-repeat, + var(--magenta-11); +} +.sib-empty-text { font-size: 18px; line-height: 1.5; color: var(--ink-86); } +.sib-empty-text strong { color: var(--ink); } +@media (max-width: 800px) { .sib-empty { grid-template-columns: minmax(0, 1fr); justify-items: start; } } +/* long inline paths and commands wrap instead of running out of their card */ +.feature-list code, .card-text code, .step code, .check-list code, .split-list code, .compare-table code, .sib-desc code, .term-note code, .prose :not(pre) > code { overflow-wrap: anywhere; } + +/* the hero's screen recording: a window the full width of the container */ +.hero-video { margin-top: 64px; } +.video-window { + border-radius: var(--radius); overflow: hidden; + background: var(--navy-deep); + box-shadow: inset 0 0 0 1px var(--on-dark-16), 0 40px 80px -24px #000000a6; +} +.video-window .log-bar { justify-content: flex-start; gap: 14px; } +.video-screen { display: block; width: 100%; height: auto; aspect-ratio: 32 / 9; background: #000; } +@media (max-width: 1000px) { .hero-video { margin-top: 48px; } } diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..4d28170 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..f41d6db --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/images/coredesktop-logo.svg b/static/images/coredesktop-logo.svg new file mode 100644 index 0000000..645d721 --- /dev/null +++ b/static/images/coredesktop-logo.svg @@ -0,0 +1,18 @@ + + CoreDesktop + + + + + + + + + + + + + + + + diff --git a/static/video/ultrawide-desktop.jpg b/static/video/ultrawide-desktop.jpg new file mode 100644 index 0000000..8712d27 Binary files /dev/null and b/static/video/ultrawide-desktop.jpg differ diff --git a/static/video/ultrawide-desktop.mp4 b/static/video/ultrawide-desktop.mp4 new file mode 100644 index 0000000..c9b5a31 Binary files /dev/null and b/static/video/ultrawide-desktop.mp4 differ diff --git a/static/video/ultrawide-desktop.webm b/static/video/ultrawide-desktop.webm new file mode 100644 index 0000000..2f512cc Binary files /dev/null and b/static/video/ultrawide-desktop.webm differ diff --git a/tools/__pycache__/make-logo.cpython-313.pyc b/tools/__pycache__/make-logo.cpython-313.pyc new file mode 100644 index 0000000..c1d19f9 Binary files /dev/null and b/tools/__pycache__/make-logo.cpython-313.pyc differ diff --git a/tools/make-logo.py b/tools/make-logo.py new file mode 100644 index 0000000..8734b05 --- /dev/null +++ b/tools/make-logo.py @@ -0,0 +1,75 @@ +"""Draw the CoreDesktop wordmark in the construction of the Federated wordmark. + +Measured from Federated's logo.png (775x129): every glyph sits in a 58x57 box +starting at y=55, strokes are 13px, outer corners ~16px radius. Glyphs here are +centerline paths stroked at 13px with 9.5px arcs (16px outside, ~3px inside). +""" +import sys + +S = 13 # stroke +H = S / 2 # half stroke: centerline inset +R = 9.5 # centerline corner radius +W, TOP = 58, 55 # glyph box width, top y (box height 57) +ADV = W + 16 # advance: box + gap +L, RT = H, W - H # left / right centerlines +T, B = H, 57 - H # top / bottom centerlines +M = 29 # middle bar centerline (e, s, k) + + +def a(x, y, sweep=1): + return f"A{R} {R} 0 0 {sweep} {x:g} {y:g}" + + +GLYPHS = { + # e: closed upper bowl down to the middle bar, open lower right + "e": f"M47 {B} H{L+R} {a(L, B-R)} V{T+R} {a(L+R, T)} H{RT-R} {a(RT, T+R)} V{M-R} {a(RT-R, M)} H{L}", + # c: e without the bar + "c": f"M51 {T} H{L+R} {a(L, T+R, 0)} V{B-R} {a(L+R, B, 0)} H51", + # o: a closed rounded box + "o": f"M{L+R} {T} H{RT-R} {a(RT, T+R)} V{B-R} {a(RT-R, B)} H{L+R} {a(L, B-R)} V{T+R} {a(L+R, T)} Z", + # r: full stem, top arc, short hook on the right + "r": f"M{L} 57 V{T+R} {a(L+R, T)} H{RT-R} {a(RT, T+R)} V27", + # d: lower bowl with a full-height stem on the right (bowl top at y=12) + "d": f"M{RT} 0 V{B-R} {a(RT-R, B)} H{L+R} {a(L, B-R)} V{12+H+R} {a(L+R, 12+H)} H{RT}", + # p: d turned around: upper bowl, full-height stem on the left + "p": f"M{L} 57 V{T+R} {a(L+R, T)} H{RT-R} {a(RT, T+R)} V{45-H-R} {a(RT-R, 45-H)} H{L}", + # s: top bar, upper left, middle bar, lower right, bottom bar + "s": f"M52 {T} H{L+R} {a(L, T+R, 0)} V{M-R} {a(L+R, M, 0)} H{RT-R} {a(RT, M+R)} V{B-R} {a(RT-R, B)} H6", + # k: full stem, two diagonal arms meeting it at the middle. The arms run + # past the box and are clipped to it (see CLIPPED), so their ends are flat. + "k": f"M{L} 0 V57 M{L} 28.5 L62 -9 M{L} 28.5 L62 66", + # t: offset stem, crossbar near the top, bottom bar hooking up on the right + "t": f"M17 0 V{B-R} {a(17+R, B, 0)} H{RT-R} M{RT} 34 V{B-R} {a(RT-R, B)} M0 16 H{W}", +} + +STAR = ("M350 616 c-26 -126 -109 -201 -252 -231 -19 -3 -28 -12 -28 -24 0 -14 10 -20 45 -24 " + "118 -17 219 -119 240 -245 5 -31 11 -42 25 -42 13 0 19 11 24 48 8 58 55 142 99 176 " + "47 36 117 66 155 66 25 0 32 4 32 19 0 15 -11 22 -50 30 -73 16 -122 44 -168 96 " + "-40 46 -72 117 -72 162 0 16 -6 23 -19 23 -16 0 -22 -11 -31 -54z") + + +# glyphs whose strokes are clipped to the glyph box +CLIPPED = {"k"} + + +def svg(word, star_fill, ink, x0=75): + paths = [] + for i, ch in enumerate(word): + clip = ' clip-path="url(#glyph-box)"' if ch in CLIPPED else "" + paths.append(f'') + width = x0 + len(word) * ADV - 16 + return f''' + CoreDesktop + + + + {chr(10).join(" " + p for p in paths).strip()} + + +''' + + +if __name__ == "__main__": + # python3 tools/make-logo.py static/images/coredesktop-logo.svg + out = sys.argv[1] + open(out, "w").write(svg("coredesktop", "#ff3daf", "#ffffff"))