Compare commits
No commits in common. "221f6cccce6708f4e1a9502a09f4ce0feb0464d5" and "e1ee05bd4168915106af740871c38dbeb7f41ffa" have entirely different histories.
221f6cccce
...
e1ee05bd41
2
.gitignore
vendored
|
|
@ -7,8 +7,6 @@
|
|||
!docs/
|
||||
!docs/LICENSE
|
||||
!docs/README*
|
||||
!docs/branding/
|
||||
!docs/branding/**
|
||||
!crates
|
||||
!crates/**
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Project Overview
|
||||
|
||||
archivr is a self-hosted archival tool that captures and preserves digital content — YouTube/Twitter/Instagram/TikTok/Reddit posts, arbitrary URLs, full web pages (via SingleFile + Chromium, optionally through a Freedium mirror for paywalled articles), and local files — into self-contained, SQLite-backed archive directories with blob deduplication, hierarchical tags, collections, and role-based auth. Rust workspace + React frontend.
|
||||
Archivr is a self-hosted archival tool that captures and preserves digital content — YouTube/Twitter/Instagram/TikTok/Reddit posts, arbitrary URLs, full web pages (via SingleFile + Chromium, optionally through a Freedium mirror for paywalled articles), and local files — into self-contained, SQLite-backed archive directories with blob deduplication, hierarchical tags, collections, and role-based auth. Rust workspace + React frontend.
|
||||
|
||||
Read `ARCHIVR-MENTAL-MODEL.md` before making structural changes.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ This document explains the current project shape after the workspace refactor.
|
|||
|
||||
## The Big Model
|
||||
|
||||
archivr is now a Rust workspace with three crates:
|
||||
Archivr is now a Rust workspace with three crates:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
|
|
|||
|
|
@ -387,7 +387,8 @@ pub fn app_with_state(state: AppState) -> Router {
|
|||
get(blob_cleanup_scan_handler).delete(blob_cleanup_delete_handler),
|
||||
)
|
||||
.route("/api/util/resolve-tco", post(resolve_tco_handler))
|
||||
.fallback_service(ServeDir::new(&static_dir).not_found_service(ServeFile::new(static_dir.join("index.html"))))
|
||||
.nest_service("/assets", ServeDir::new(static_dir.join("assets")))
|
||||
.fallback_service(ServeFile::new(static_dir.join("index.html")))
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
setup_guard,
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 295 KiB |
|
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" role="img" aria-label="Archivr">
|
||||
<defs>
|
||||
<style>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');</style>
|
||||
</defs>
|
||||
<rect width="48" height="48" rx="7.68" fill="#8d3f30"/>
|
||||
<text x="24" y="24" text-anchor="middle" dominant-baseline="central"
|
||||
dx="0.02em" dy="0.03em"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="36" fill="#f4ead8">A</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 548 B |
|
|
@ -4,8 +4,6 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Archivr</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<script type="module" crossorigin src="/assets/index-B67momER.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DicH9MNh.css">
|
||||
</head>
|
||||
|
|
|
|||
445
docs/README.md
|
|
@ -1,215 +1,192 @@
|
|||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="branding/assets/banner-dark.png">
|
||||
<img alt="Archivr — Preserve what matters. Forever." src="branding/assets/banner-light.svg" width="860">
|
||||
</picture>
|
||||
</p>
|
||||
# archivr
|
||||
|
||||
<p align="center">
|
||||
<a href="../LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-8d3f30?style=flat-square" alt="MIT License"></a>
|
||||
<img src="https://img.shields.io/badge/rust-2024_edition-b78342?style=flat-square&logo=rust&logoColor=white" alt="Rust 2024">
|
||||
<img src="https://img.shields.io/badge/self--hosted-yes-245f72?style=flat-square" alt="Self-hosted">
|
||||
</p>
|
||||
An open-source self-hosted archiving tool. Work in progress.
|
||||
|
||||
---
|
||||
- [x] Archiving
|
||||
- [x] Archiving media files from social media platforms
|
||||
- [x] YouTube Videos
|
||||
- [x] YouTube Playlists
|
||||
- [x] YouTube Channels
|
||||
- [x] Twitter Videos
|
||||
- [x] Instagram
|
||||
- [x] Facebook
|
||||
- [x] TikTok
|
||||
- [x] Reddit
|
||||
- [x] Snapchat
|
||||
- [ ] YouTube Posts (postponed)
|
||||
- [x] Archiving local files
|
||||
- [x] Archiving Twitter Tweets, Threads, and Articles
|
||||
- [ ] Archiving files from cloud storage services (Google Drive, Dropbox, OneDrive) and from URLs
|
||||
- [x] URLs
|
||||
- [ ] Google Drive
|
||||
- [ ] Dropbox
|
||||
- [ ] OneDrive
|
||||
- (Some of these could be postponed for later.)
|
||||
- [x] Archive web pages (HTML, CSS, JS, images)
|
||||
- [ ] Archiving emails (???)
|
||||
- [ ] Gmail
|
||||
- [ ] Outlook
|
||||
- [ ] Yahoo Mail
|
||||
- [x] Management
|
||||
- [x] Deduplication
|
||||
- [x] Tagging system
|
||||
- [x] Search functionality
|
||||
- [ ] Categorization
|
||||
- [x] Metadata extraction and storage
|
||||
- [x] User Interface
|
||||
- [x] Web-based UI
|
||||
- [x] Authentication and login
|
||||
- [x] Archive setup
|
||||
- [x] Browse and view entries
|
||||
- [x] Tag management and filtering
|
||||
- [x] Search entries
|
||||
- [x] View archive runs
|
||||
- [x] Capture dialog
|
||||
- [x] User settings and API tokens
|
||||
- [x] Admin panel
|
||||
- [ ] Backup and Sync
|
||||
- [ ] Cloud backup (AWS S3, Google Cloud Storage)
|
||||
- [ ] Local backup
|
||||
|
||||
Archivr is a self-hosted tool for capturing and preserving digital content — YouTube videos and playlists, tweets and threads, Instagram, TikTok, web pages, and local files — into self-contained, locally-owned archives. Content is stored in SQLite with SHA3-256 blob deduplication, hierarchical tags, a browser-based UI, and role-based auth.
|
||||
## Motivation
|
||||
|
||||
## Table of Contents
|
||||
There are two driving factors behind this project:
|
||||
|
||||
- [Features](#features)
|
||||
- [Quick Start](#quick-start)
|
||||
- [With Nix](#with-nix)
|
||||
- [With Docker](#with-docker-1)
|
||||
- [Architecture](#architecture)
|
||||
- [Supported Inputs](#supported-inputs)
|
||||
- [YouTube playlists and channels](#youtube-playlists-and-channels)
|
||||
- [Video quality and audio-only](#video-quality-and-audio-only)
|
||||
- [Configuration](#configuration)
|
||||
- [TOML config file](#toml-config-file)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Deployment](#deployment)
|
||||
- [Security](#security)
|
||||
- [NixOS](#hosting-on-nixos)
|
||||
- [Docker](#hosting-with-docker)
|
||||
- [Development](#development)
|
||||
- [License](#license)
|
||||
- In the age of information, all data is ephemeral. Social media platforms frequently delete content, and cloud storage services can become inaccessible and unreliable. Being able to archive important data is _very important_ for preserving personal memories and digital history.
|
||||
- I will be creating a small encyclopedia for my future family and kids. Therefore, I want to make sure that all the information I gather is preserved and accessible for future reference.
|
||||
|
||||
## Features
|
||||
This project aims to provide a reliable solution for archiving important data from various sources, ensuring that users can preserve their digital assets for the long term.
|
||||
|
||||
- **Social media** — YouTube (videos, shorts, playlists, channels with sync mode), X/Twitter (tweet and thread JSON + media downloads), Instagram, TikTok, Facebook, Reddit, Snapchat via yt-dlp
|
||||
- **Web pages** — full self-contained HTML snapshots via SingleFile + Chromium; optional Freedium mirror for paywalled articles; reader mode
|
||||
- **Local files** — import any file from disk by `file://` path
|
||||
- **Deduplication** — SHA3-256 content-addressed blob store shared across all captures; identical files are stored once
|
||||
- **Tags and search** — hierarchical tag tree, full-text search, filterable entry list
|
||||
- **Multiple archives** — the server mounts any number of separate archives from a single TOML config
|
||||
- **Role-based auth** — Guest / User / Admin / Owner roles; session cookies and API tokens; Argon2 passwords
|
||||
- **Quality selection** — choose video quality or audio-only per capture; a live metadata probe populates the selector before download
|
||||
## Archive Inputs
|
||||
|
||||
## Quick Start
|
||||
`archivr archive <path>` currently accepts three kinds of inputs:
|
||||
|
||||
### With Nix
|
||||
- Local files via `file://...`
|
||||
- Direct platform URLs
|
||||
- Platform shorthand inputs such as `tweet:...`, `yt:...`, or `instagram:...`
|
||||
|
||||
## Running Archivr
|
||||
|
||||
Archivr currently ships as two binaries:
|
||||
|
||||
- `archivr`
|
||||
- The CLI for creating and writing to one archive.
|
||||
- Use this for `init` and `archive`.
|
||||
- `archivr-server`
|
||||
- The web server for reading one or more existing archives through the browser UI.
|
||||
- Use this after archives already exist.
|
||||
|
||||
With Nix, run the CLI with:
|
||||
|
||||
```sh
|
||||
# Create an archive
|
||||
nix run github:thegeneralist/archivr#archivr -- init ./my-archive --name "My Archive"
|
||||
|
||||
# Archive something
|
||||
nix run github:thegeneralist/archivr#archivr -- archive https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
|
||||
# Start the web UI (reads ./archivr-server.toml)
|
||||
nix run github:thegeneralist/archivr#archivr-server
|
||||
nix run .#archivr -- init ./my-archive --name "My Archive"
|
||||
nix run .#archivr -- archive file:///absolute/path/to/file.pdf
|
||||
```
|
||||
|
||||
Create `archivr-server.toml` next to where you run the command:
|
||||
Run the web server with:
|
||||
|
||||
```sh
|
||||
nix run .#archivr-server -- ./archivr-server.toml
|
||||
```
|
||||
|
||||
The server expects a TOML registry file. If no path is passed, it reads `./archivr-server.toml`.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
auth_db_path = "/absolute/path/to/archivr-auth.sqlite"
|
||||
|
||||
[[archives]]
|
||||
id = "personal"
|
||||
label = "Personal"
|
||||
archive_path = "/absolute/path/to/my-archive/.archivr"
|
||||
```
|
||||
|
||||
Then open `http://127.0.0.1:8080`. On the first visit you will be prompted to create the owner account.
|
||||
Then open:
|
||||
|
||||
### With Docker
|
||||
```text
|
||||
http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
When installed through Nix, `archivr-server` is wrapped so it can find the static web UI assets automatically. The wrapper sets `ARCHIVR_STATIC_DIR` to the installed static asset directory. Running from source with `cargo run -p archivr-server` falls back to `crates/archivr-server/static`.
|
||||
|
||||
### Security and Deployment
|
||||
|
||||
`archivr-server` is a **local-only tool by default**. It binds to `127.0.0.1:8080` and has no authentication or access control. Do not expose it to a public network or a shared LAN without understanding the risks.
|
||||
|
||||
**Changing the bind address**
|
||||
|
||||
You can set the bind address in your TOML config:
|
||||
|
||||
```toml
|
||||
# Optional. Default: 127.0.0.1:8080
|
||||
# Only change this if you know what you are doing — the server has no authentication.
|
||||
bind = "127.0.0.1:9090"
|
||||
```
|
||||
|
||||
Or override it with the `ARCHIVR_BIND` environment variable:
|
||||
|
||||
```sh
|
||||
mkdir config
|
||||
cp docker/config.example.toml config/archivr-server.toml
|
||||
# Edit config/archivr-server.toml
|
||||
|
||||
# Initialize the archive on the persistent volume (run once)
|
||||
docker compose run --rm archivr archivr init \
|
||||
/data/archives/main /data/archives/main/.archivr/store \
|
||||
--name "Main Archive"
|
||||
|
||||
docker compose up -d
|
||||
ARCHIVR_BIND=127.0.0.1:9090 nix run .#archivr-server -- ./archivr-server.toml
|
||||
```
|
||||
|
||||
Open `http://localhost:8080`. See [Hosting with Docker](#hosting-with-docker) for volume layout and Twitter/X credential setup.
|
||||
If the server is started with a non-loopback address (e.g. `0.0.0.0`), it prints a warning to stderr:
|
||||
|
||||
## Architecture
|
||||
|
||||
Two binaries:
|
||||
|
||||
| Binary | Purpose |
|
||||
|---|---|
|
||||
| `archivr` | CLI — create archives (`init`) and add content (`archive`) |
|
||||
| `archivr-server` | Web server — browse and search one or more archives via browser UI |
|
||||
|
||||
Archive layout created by `archivr init`:
|
||||
|
||||
```
|
||||
my-archive/
|
||||
├── .archivr/ # metadata: name, store_path, archivr.sqlite
|
||||
└── store/
|
||||
├── raw/ # deduplicated blobs: raw/A/B/<sha3-256>.ext
|
||||
├── raw_tweets/ # tweet and thread JSON
|
||||
├── structured/ # structured metadata outputs
|
||||
└── temp/ # staging area during capture
|
||||
```text
|
||||
warn: archivr-server is bound to 0.0.0.0:8080 — this server has no authentication. Only expose it on a trusted network.
|
||||
```
|
||||
|
||||
A separate auth database (`archivr-auth.sqlite`, path set in TOML) holds users, sessions, API tokens, and role bits. It is independent of individual archives.
|
||||
**When will auth be added?**
|
||||
|
||||
## Supported Inputs
|
||||
Auth and session handling will be designed when remote or public hosting becomes a real requirement. Until then, keep the server on loopback. See `crates/archivr-server/src/routes.rs` for the route classification that will guide where middleware is applied.
|
||||
|
||||
`archivr archive <locator>` accepts URLs and platform shorthands:
|
||||
### Supported Platforms
|
||||
|
||||
| Platform | Input examples |
|
||||
|---|---|
|
||||
| Local file | `file:///absolute/path/to/file.pdf` |
|
||||
| YouTube video / short | `https://youtube.com/watch?v=ID` · `yt:video/ID` · `yt:short/ID` |
|
||||
| YouTube playlist | `https://youtube.com/playlist?list=ID` |
|
||||
| YouTube channel | `https://youtube.com/@handle` |
|
||||
| X/Twitter tweet (JSON) | `tweet:ID` · `x:tweet:ID` · `twitter:tweet:ID` |
|
||||
| X/Twitter thread (JSON) | `x:thread:ID` · `twitter:thread:ID` |
|
||||
| X/Twitter media download | `tweet:media:ID` |
|
||||
| Instagram | Direct URL · `instagram:ID` |
|
||||
| TikTok | Direct URL · `tiktok:ID` |
|
||||
| Facebook | Direct URL · `facebook:ID` |
|
||||
| Reddit | Direct URL · `reddit:ID` |
|
||||
| Snapchat | Direct URL · `snapchat:ID` |
|
||||
| Arbitrary URL / web page | Any `https://` URL |
|
||||
- Local files: `file:///absolute/path/to/file.ext`
|
||||
- YouTube media: individual videos/shorts, playlists, and channels; standard URLs or [shorthand video inputs](#supported-shorthand-inputs). Playlists and channels archive as a container entry with each video stored as a child entry beneath it.
|
||||
- X/Twitter media from Tweets: normal Tweet URLs or the `tweet:media:ID` shorthand
|
||||
- X/Twitter Tweet content scrape: [Tweet and Thread shorthands](#supported-shorthand-inputs). (These are saved as JSON files in `raw_tweets/`)
|
||||
- Instagram, Facebook, TikTok, Reddit, Snapchat: direct URLs or platform-prefixed shorthand passed through to `yt-dlp`
|
||||
|
||||
### YouTube playlists and channels
|
||||
#### Video quality and audio-only downloads
|
||||
|
||||
Capturing a playlist or channel creates a **container entry** with each video archived as a child beneath it. Before downloading, the UI probes each video for available quality options — set quality per-video or apply one to the whole batch. Individual videos can be excluded with the remove button.
|
||||
|
||||
**Sync mode:** when re-archiving a playlist or channel, enable sync mode in the capture dialog to skip videos that are already in the archive. Only new videos are downloaded; the existing container is reused.
|
||||
|
||||
### Video quality and audio-only
|
||||
|
||||
When capturing a yt-dlp-backed source through the web UI, a metadata probe runs first and populates the quality selector with heights actually available in that video:
|
||||
When capturing via the web UI, entering a URL for a yt-dlp-backed source (YouTube, Instagram, TikTok, Facebook, Reddit, Snapchat, X media) triggers a metadata probe via `GET /api/archives/:id/captures/probe`. The quality selector then shows only the heights actually available in that video plus **Best quality** (default). An **Audio only** option is appended whenever the probe confirms an audio track exists. UI behaviour by probe outcome:
|
||||
|
||||
| `qualities` | `has_audio` | UI shows |
|
||||
|---|---|---|
|
||||
| `["1080p", "720p", …]` | `true` | Best / heights / Audio only |
|
||||
| `["1080p", …]` | `false` | Best / heights |
|
||||
| `[]` | `true` | Audio only (pre-selected) |
|
||||
| `[]` | `true` | Audio only (pre-selected, no Best option) |
|
||||
| `[]` | `false` | "No media detected" |
|
||||
| probe fails (502) | — | picker hidden; capture still submittable |
|
||||
| probe fails (502) | — | picker hidden, capture still submittable |
|
||||
|
||||
The `POST /api/archives/:id/captures` endpoint accepts an optional `quality` field:
|
||||
The `POST /api/archives/:id/captures` endpoint accepts an optional `quality` field: `"best"`, `"audio"`, or any `"NNNp"` height string:
|
||||
|
||||
```json
|
||||
{ "locator": "https://www.youtube.com/watch?v=...", "quality": "720p" }
|
||||
{ "locator": "https://www.youtube.com/watch?v=...", "quality": "audio" }
|
||||
```
|
||||
|
||||
`"audio"` selects the most efficient native audio track without re-encoding (Opus/WebM preferred, then AAC/M4A). Omitting `quality` or passing `"best"` downloads at the highest available quality.
|
||||
`"audio"` selects the most efficient native audio track without transcoding: Opus/WebM is preferred (smallest at equivalent quality), then AAC/M4A, then whatever yt-dlp considers best. The saved file's extension matches the native format (`.webm` for Opus, `.m4a` for AAC, etc.) — no ffmpeg re-encode, no size inflation. Any `"NNNp"` height is accepted; the server builds the yt-dlp format selector with an unconditional `/best` fallback so the download succeeds even if the exact height is unavailable. Omitting `quality` or passing `"best"` downloads at the highest available quality. Anything else is rejected with HTTP 400.
|
||||
|
||||
## Configuration
|
||||
|
||||
### TOML config file
|
||||
|
||||
```toml
|
||||
# Optional. Default: 127.0.0.1:8080
|
||||
bind = "127.0.0.1:8080"
|
||||
|
||||
# Required. Persists across upgrades; must be on a writable path.
|
||||
auth_db_path = "/var/lib/archivr/archivr-auth.sqlite"
|
||||
|
||||
[[archives]]
|
||||
id = "personal"
|
||||
label = "Personal"
|
||||
archive_path = "/srv/archivr/personal/.archivr"
|
||||
|
||||
[[archives]]
|
||||
id = "work"
|
||||
label = "Work"
|
||||
archive_path = "/srv/archivr/work/.archivr"
|
||||
The probe endpoint (`GET /api/archives/:id/captures/probe?locator=…`) requires auth and returns 200 with:
|
||||
```json
|
||||
{ "has_video": true, "has_audio": true, "qualities": ["1080p", "720p", "480p"] }
|
||||
{ "has_video": false, "has_audio": true, "qualities": [] }
|
||||
{ "has_video": false, "has_audio": false, "qualities": [] }
|
||||
```
|
||||
`has_video: false, has_audio: false` means yt-dlp found no downloadable tracks (e.g. a tweet with no media). A 502 means yt-dlp itself failed (transient network error, rate-limit, unsupported extractor) — treat as inconclusive, not "no media."
|
||||
|
||||
See `docker/config.example.toml` for a complete annotated example.
|
||||
#### YouTube playlists and channels
|
||||
|
||||
### Environment variables
|
||||
Capturing a YouTube playlist or channel URL creates a **container entry** for the playlist or channel, with each video archived as a child entry beneath it. Before downloading, the capture UI probes each video to fetch available quality options, letting you set quality per-video or apply a single quality to the whole playlist.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `ARCHIVR_BIND` | `127.0.0.1:8080` | Bind address; overrides `bind` in TOML |
|
||||
| `ARCHIVR_STATIC_DIR` | `crates/archivr-server/static` | Pre-built frontend asset directory |
|
||||
| `ARCHIVR_YT_DLP` | `yt-dlp` | yt-dlp binary used for video and social downloads |
|
||||
| `ARCHIVR_SINGLE_FILE` | `single-file` | single-file-cli binary for web page archiving |
|
||||
| `ARCHIVR_CHROME` | `chromium` | Chromium executable passed to single-file |
|
||||
| `ARCHIVR_CHROME_ARGS` | — | Extra space-separated Chromium flags (Docker sets `--no-sandbox`) |
|
||||
| `ARCHIVR_TWITTER_CREDENTIALS_FILE` | — | Cookies file for tweet/thread scraping — required for `tweet:ID` and `x:thread:ID` inputs |
|
||||
| `ARCHIVR_TWEET_SCRAPER` | `vendor/twitter/scrape_user_tweet_contents.py` | Tweet scraper script path |
|
||||
| `ARCHIVR_TWEET_PYTHON` | `python3` | Python executable for the tweet scraper |
|
||||
**Incremental sync:** When re-archiving a playlist or channel, enable **sync mode** in the capture dialog to skip videos that are already in the archive. Only new videos are downloaded; the existing container entry is reused.
|
||||
|
||||
The Nix wrapper and Docker image set `ARCHIVR_STATIC_DIR`, `ARCHIVR_SINGLE_FILE`, and `ARCHIVR_CHROME` automatically.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Security
|
||||
|
||||
`archivr-server` binds to `127.0.0.1:8080` by default. Do not expose it to a public network without understanding the risks. When started on a non-loopback address the server logs a warning to stderr.
|
||||
**Excluding individual videos:** In the expanded per-video list, each video has a remove button (×) to exclude it from the current capture. Removed videos are not downloaded; the rest proceed normally.
|
||||
|
||||
### Hosting on NixOS
|
||||
|
||||
The flake exposes `nixosModules.default`:
|
||||
The flake exposes a `nixosModules.default` output. Add it to your system flake and
|
||||
enable the service:
|
||||
|
||||
```nix
|
||||
# flake.nix (your system flake)
|
||||
|
|
@ -223,7 +200,7 @@ The flake exposes `nixosModules.default`:
|
|||
{
|
||||
services.archivr-server = {
|
||||
enable = true;
|
||||
# listenAddress defaults to "127.0.0.1"
|
||||
# listenAddress defaults to "127.0.0.1" (loopback only)
|
||||
# port defaults to 8080
|
||||
archives = [
|
||||
{ id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; }
|
||||
|
|
@ -237,73 +214,161 @@ The flake exposes `nixosModules.default`:
|
|||
}
|
||||
```
|
||||
|
||||
The module creates an `archivr` system user and group, generates the TOML config from your options, stores the auth database at `/var/lib/archivr-server/` (persists across upgrades), and runs under a hardened systemd unit (`ProtectSystem = strict`, `NoNewPrivileges`, `PrivateTmp`). Archive directories are whitelisted for read-write access.
|
||||
The module:
|
||||
- Creates an `archivr` system user and group.
|
||||
- Generates the TOML config from your options and stores the auth database under
|
||||
`/var/lib/archivr-server/` (persists across upgrades).
|
||||
- Runs under a hardened systemd unit (`ProtectSystem = strict`, `NoNewPrivileges`,
|
||||
`PrivateTmp`, etc.). Archive directories are whitelisted for read-write access.
|
||||
- Restarts automatically on failure.
|
||||
|
||||
Set `openFirewall = true` with a non-loopback `listenAddress` only when LAN or remote access is required.
|
||||
**`openFirewall`** — set to `true` to open the TCP port derived from `bind`.
|
||||
Only needed when binding to a non-loopback address:
|
||||
|
||||
```nix
|
||||
services.archivr-server = {
|
||||
listenAddress = "0.0.0.0";
|
||||
port = 8080; # explicit, though 8080 is the default
|
||||
openFirewall = true;
|
||||
};
|
||||
```
|
||||
|
||||
**Archive directories** must be readable and writable by the `archivr` user.
|
||||
Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`.
|
||||
|
||||
Archive directories must be owned by the `archivr` user. Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`.
|
||||
|
||||
### Hosting with Docker
|
||||
|
||||
A `Dockerfile` and `docker-compose.yml` are provided for self-hosting without Nix.
|
||||
|
||||
**Quickstart**
|
||||
|
||||
1. Copy the example config and edit it:
|
||||
|
||||
```sh
|
||||
# 1. Configure
|
||||
mkdir config
|
||||
cp docker/config.example.toml config/archivr-server.toml
|
||||
# Edit archivr-server.toml — set id, label, archive_path, and auth_db_path
|
||||
# edit config/archivr-server.toml — set archive id, label, and archive_path
|
||||
```
|
||||
|
||||
# 2. Initialize each archive (run once per archive)
|
||||
docker compose run --rm archivr archivr init \
|
||||
/data/archives/main /data/archives/main/.archivr/store \
|
||||
--name "Main Archive"
|
||||
2. Initialize each archive on the persistent data volume before the first start.
|
||||
The image includes the `archivr` CLI for this purpose:
|
||||
|
||||
# 3. Start
|
||||
```sh
|
||||
docker compose run --rm archivr archivr init /data/archives/main /data/archives/main/.archivr/store --name "Main Archive"
|
||||
```
|
||||
|
||||
This creates `/data/archives/main/.archivr/` with the metadata the server requires.
|
||||
A bare `mkdir` is not enough — the server reads `name` and `store_path` files that
|
||||
only `archivr init` writes.
|
||||
|
||||
3. Start the server:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Then open `http://localhost:8080`.
|
||||
|
||||
**Volumes**
|
||||
|
||||
| Mount | Purpose |
|
||||
|---|---|
|
||||
|-------|---------|
|
||||
| `./config` (read-only) | Directory containing `archivr-server.toml` |
|
||||
| `archivr-data` named volume | Auth database (`/data/archivr-auth.sqlite`) and archive directories |
|
||||
|
||||
> **Important:** `auth_db_path` must point to a path on the writable data volume (e.g. `/data/archivr-auth.sqlite`). The example config sets this correctly. A bare `mkdir` is not enough to initialise an archive — `archivr init` writes metadata files the server requires.
|
||||
> **Important:** `auth_db_path` must be set explicitly in `archivr-server.toml` to a
|
||||
> path on the writable data volume (e.g. `/data/archivr-auth.sqlite`). If left unset,
|
||||
> the server defaults to writing the auth database next to the config file — which is
|
||||
> on the read-only `/config` mount and will fail. The example config sets this correctly.
|
||||
|
||||
**Twitter/X archiving:** supply a cookies file inside the config volume and reference it in `docker-compose.yml`:
|
||||
**Twitter/X archiving**
|
||||
|
||||
Supply a cookies file inside the config volume and set `ARCHIVR_TWITTER_CREDENTIALS_FILE` in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
ARCHIVR_TWITTER_CREDENTIALS_FILE: /config/twitter-cookies.txt
|
||||
```
|
||||
|
||||
**Building locally:**
|
||||
**Building the image locally**
|
||||
|
||||
```sh
|
||||
docker build -t archivr-server .
|
||||
```
|
||||
|
||||
The image compiles the Rust binary in a separate build stage; only runtime dependencies (Chromium, Node.js, Python) land in the final layer.
|
||||
The image compiles the Rust binary in a separate build stage so only the runtime
|
||||
dependencies (Chromium, Node.js, Python) land in the final layer.
|
||||
|
||||
## Development
|
||||
### Supported Shorthand Inputs
|
||||
|
||||
Runtime dependencies beyond Rust and Node: `yt-dlp`, Chromium, `single-file` (Node), Python 3 with `twitter-api-client`, `ffmpeg`. `nix develop` provides the dev subset.
|
||||
- YouTube video/short media:
|
||||
- `yt:video/ID`
|
||||
- `youtube:video/ID`
|
||||
- `yt:short/ID`
|
||||
- `yt:shorts/ID`
|
||||
- `youtube:shorts/ID`
|
||||
- X/Twitter tweet JSON content:
|
||||
- `tweet:ID`
|
||||
- `x:tweet:ID`
|
||||
- `x:x:ID`
|
||||
- `twitter:x:ID`
|
||||
- `twitter:tweet:ID`
|
||||
- X/Twitter media/video download:
|
||||
- `tweet:media:ID`
|
||||
- X/Twitter thread JSON content:
|
||||
- `x:thread:ID`
|
||||
- `twitter:thread:ID`
|
||||
- Other platform shorthands:
|
||||
- `instagram:ID`
|
||||
- `facebook:ID`
|
||||
- `tiktok:ID`
|
||||
- `reddit:ID`
|
||||
- `snapchat:ID`
|
||||
|
||||
```sh
|
||||
# Rust (workspace root)
|
||||
cargo build
|
||||
cargo test
|
||||
cargo test -p archivr-core
|
||||
cargo run -p archivr-server -- ./archivr-server.toml
|
||||
### Environment Variables
|
||||
|
||||
# Frontend (from frontend/)
|
||||
bun install
|
||||
bun run dev # Vite dev server
|
||||
bun run build # → crates/archivr-server/static/
|
||||
bun run storybook # Component QA on :6006
|
||||
- `ARCHIVR_BIND`
|
||||
- Optional.
|
||||
- Overrides the bind address from the TOML config. Useful in Docker where you need
|
||||
`0.0.0.0:8080` without editing the config file. Default: `127.0.0.1:8080`.
|
||||
- `ARCHIVR_STATIC_DIR`
|
||||
- Optional.
|
||||
- Path to the directory of pre-built frontend assets served by the web UI.
|
||||
Set automatically by the Nix wrapper and the Docker image. When running from
|
||||
source with `cargo run`, falls back to `crates/archivr-server/static`.
|
||||
- `ARCHIVR_YT_DLP`
|
||||
- Optional.
|
||||
- Overrides the `yt-dlp` binary used for YouTube, X media posts, Instagram, Facebook, TikTok, Reddit, and Snapchat downloads.
|
||||
- `ARCHIVR_SINGLE_FILE`
|
||||
- Optional.
|
||||
- Overrides the `single-file` binary used for web page archiving. Set automatically by the Nix wrapper and the Docker image.
|
||||
- `ARCHIVR_CHROME`
|
||||
- Optional.
|
||||
- Overrides the Chromium/Chrome executable passed to `single-file` via `--browser-executable-path`. Set automatically by the Nix wrapper and the Docker image. Default: `chromium`.
|
||||
- `ARCHIVR_CHROME_ARGS`
|
||||
- Optional.
|
||||
- Space-separated extra flags appended to Chromium's `--browser-args`. The Docker
|
||||
image sets this to `--no-sandbox` because Chromium refuses to run as root without
|
||||
it. Leave unset when running natively (Nix, Linux desktop).
|
||||
A `--window-size=1920,1080` is always passed to provide a realistic desktop
|
||||
viewport (so responsive @media rules and styles are evaluated and preserved
|
||||
correctly). Supply your own `--window-size=...` here to override.
|
||||
- `ARCHIVR_TWITTER_CREDENTIALS_FILE`
|
||||
- Required for tweet/thread scraping inputs such as `tweet:ID` and `x:thread:ID`.
|
||||
- Must point to a cookies file for the vendored scraper.
|
||||
- `ARCHIVR_TWEET_SCRAPER`
|
||||
- Optional.
|
||||
- Overrides the tweet scraper script path. Default: `vendor/twitter/scrape_user_tweet_contents.py`.
|
||||
- `ARCHIVR_TWEET_PYTHON`
|
||||
- Optional.
|
||||
- Overrides the Python executable used to run the tweet scraper. Default: `python3`.
|
||||
|
||||
# Nix
|
||||
nix develop # dev shell
|
||||
nix build .#archivr-server
|
||||
```
|
||||
### Current Limitations
|
||||
|
||||
- Arbitrary `http://` or `https://` URLs that return HTML are archived as self-contained single-file HTML snapshots via `single-file-cli` (requires Chromium). Plain file URLs (PDFs, images, zips, etc.) are downloaded directly. Requires `single-file` and a Chromium binary on PATH, or the `ARCHIVR_SINGLE_FILE` / `ARCHIVR_CHROME` env vars set.
|
||||
- Local files currently need to be passed as `file://...` paths.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](../LICENSE.md).
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) file for details.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1,011 KiB |
|
|
@ -1,22 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 140" width="860" height="140" role="img" aria-label="Archivr — Preserve what matters. Forever.">
|
||||
<!-- Background -->
|
||||
<rect width="860" height="140" fill="#141d18"/>
|
||||
<!-- Top accent rule -->
|
||||
<rect width="860" height="2" fill="#8d3f30"/>
|
||||
|
||||
<!-- Mark -->
|
||||
<rect x="319" y="29" width="52" height="52" rx="8" fill="#8d3f30"/>
|
||||
<text x="345" y="56" text-anchor="middle" dominant-baseline="central"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
font-weight="bold" font-size="34" fill="#f4ead8">A</text>
|
||||
|
||||
<!-- Wordmark -->
|
||||
<text x="383" y="55" text-anchor="start" dominant-baseline="central"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
font-weight="normal" font-size="32" fill="#f4ead8">archivr</text>
|
||||
|
||||
<!-- Tagline -->
|
||||
<text x="430" y="107" text-anchor="middle" dominant-baseline="central"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
font-style="italic" font-size="13.5" fill="#b78342" letter-spacing="0.2">Preserve what matters. Forever.</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,22 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 140" width="860" height="140" role="img" aria-label="Archivr — Preserve what matters. Forever.">
|
||||
<!-- Background -->
|
||||
<rect width="860" height="140" fill="#f5f0e7"/>
|
||||
<!-- Top accent rule -->
|
||||
<rect width="860" height="2" fill="#8d3f30"/>
|
||||
|
||||
<!-- Mark -->
|
||||
<rect x="319" y="29" width="52" height="52" rx="8" fill="#8d3f30"/>
|
||||
<text x="345" y="56" text-anchor="middle" dominant-baseline="central"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
font-weight="bold" font-size="34" fill="#f4ead8">A</text>
|
||||
|
||||
<!-- Wordmark -->
|
||||
<text x="383" y="55" text-anchor="start" dominant-baseline="central"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
font-weight="normal" font-size="32" fill="#20251f">archivr</text>
|
||||
|
||||
<!-- Tagline -->
|
||||
<text x="430" y="107" text-anchor="middle" dominant-baseline="central"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
font-style="italic" font-size="13.5" fill="#6b6f66" letter-spacing="0.2">Preserve what matters. Forever.</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 295 KiB |
|
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" role="img" aria-label="Archivr">
|
||||
<defs>
|
||||
<style>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');</style>
|
||||
</defs>
|
||||
<rect width="48" height="48" rx="7.68" fill="#8d3f30"/>
|
||||
<text x="24" y="24" text-anchor="middle" dominant-baseline="central"
|
||||
dx="0.02em" dy="0.03em"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="36" fill="#f4ead8">A</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 548 B |
|
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" role="img" aria-label="Archivr mark">
|
||||
<defs>
|
||||
<style>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');</style>
|
||||
</defs>
|
||||
<rect width="48" height="48" rx="7.68" fill="#8d3f30"/>
|
||||
<text x="24" y="24" text-anchor="middle" dominant-baseline="central"
|
||||
dx="0.02em" dy="0.03em"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="36" fill="#f4ead8">A</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 553 B |
|
|
@ -1,15 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 48" width="196" height="48" role="img" aria-label="Archivr">
|
||||
<defs>
|
||||
<style>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');</style>
|
||||
</defs>
|
||||
<!-- Mark -->
|
||||
<rect x="4" y="4" width="40" height="40" rx="6.4" fill="#8d3f30"/>
|
||||
<text x="24" y="24" text-anchor="middle" dominant-baseline="central"
|
||||
dx="0.02em" dy="0.03em"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="30" fill="#f4ead8">A</text>
|
||||
<!-- Wordmark -->
|
||||
<text x="58" y="24" text-anchor="start" dominant-baseline="central"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="28" letter-spacing="0.28" fill="#f4ead8">archivr</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 819 B |
|
|
@ -1,15 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 48" width="196" height="48" role="img" aria-label="Archivr">
|
||||
<defs>
|
||||
<style>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');</style>
|
||||
</defs>
|
||||
<!-- Mark -->
|
||||
<rect x="4" y="4" width="40" height="40" rx="6.4" fill="#8d3f30"/>
|
||||
<text x="24" y="24" text-anchor="middle" dominant-baseline="central"
|
||||
dx="0.02em" dy="0.03em"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="30" fill="#f4ead8">A</text>
|
||||
<!-- Wordmark -->
|
||||
<text x="58" y="24" text-anchor="start" dominant-baseline="central"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="28" letter-spacing="0.28" fill="#20251f">archivr</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 819 B |
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 80" width="320" height="80">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');
|
||||
.wordmark {
|
||||
font-family: "Cormorant Garamond", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 42px;
|
||||
letter-spacing: 0.01em;
|
||||
fill: #f4ead8;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Background -->
|
||||
<rect width="320" height="80" fill="#141d18"/>
|
||||
<!-- Accent line bottom -->
|
||||
<rect x="0" y="77" width="320" height="3" fill="#8d3f30"/>
|
||||
<!-- Wordmark -->
|
||||
<text class="wordmark" x="28" y="53">Archivr</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 672 B |
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 80" width="320" height="80">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');
|
||||
.wordmark {
|
||||
font-family: "Cormorant Garamond", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 42px;
|
||||
letter-spacing: 0.01em;
|
||||
fill: #f4ead8;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Background -->
|
||||
<rect width="320" height="80" fill="#141d18"/>
|
||||
<!-- Accent line bottom -->
|
||||
<rect x="0" y="77" width="320" height="3" fill="#8d3f30"/>
|
||||
<!-- Wordmark -->
|
||||
<text class="wordmark" x="28" y="53">archivr</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 672 B |
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');
|
||||
.mark-letter {
|
||||
font-family: "Cormorant Garamond", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 74px;
|
||||
fill: #f4ead8;
|
||||
dominant-baseline: middle;
|
||||
text-anchor: middle;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- Accent background -->
|
||||
<rect x="4" y="4" width="112" height="112" rx="16" ry="16" fill="#8d3f30"/>
|
||||
<!-- Letter a -->
|
||||
<text class="mark-letter" x="60" y="63">a</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 652 B |
|
|
@ -1,31 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');
|
||||
.mark-letter {
|
||||
font-family: "Cormorant Garamond", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 62px;
|
||||
fill: #f4ead8;
|
||||
dominant-baseline: middle;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.wordmark {
|
||||
font-family: "Cormorant Garamond", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 46px;
|
||||
letter-spacing: 0.01em;
|
||||
fill: #f4ead8;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- Icon mark: accent background -->
|
||||
<rect x="8" y="8" width="104" height="104" rx="14" ry="14" fill="#8d3f30"/>
|
||||
<!-- The 'a' -->
|
||||
<text class="mark-letter" x="60" y="62">a</text>
|
||||
|
||||
<!-- Wordmark -->
|
||||
<text class="wordmark" x="138" y="62">archivr</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 964 B |
|
|
@ -1,954 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Archivr — Brand Guidelines</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,400&family=Spectral:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--shell: #141d18;
|
||||
--shell-2: #263330;
|
||||
--ink: #20251f;
|
||||
--paper: #f5f0e7;
|
||||
--paper-2: #e9e1d2;
|
||||
--paper-3: #fffaf0;
|
||||
--field: #fffdf7;
|
||||
--line: #d2c6b5;
|
||||
--line-soft: #e5dccd;
|
||||
--accent: #8d3f30;
|
||||
--accent-2: #b78342;
|
||||
--link: #245f72;
|
||||
--muted: #6b6f66;
|
||||
--muted-2: #8a8d83;
|
||||
--brand-text: #f4ead8;
|
||||
|
||||
--display: 'Cormorant Garamond', Georgia, serif;
|
||||
--serif: 'Spectral', Georgia, serif;
|
||||
--sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
html { background: var(--shell); color: var(--brand-text); font-family: var(--sans); font-size: 14px; -webkit-font-smoothing: antialiased; }
|
||||
|
||||
body { max-width: 900px; margin: 0 auto; padding: 0 40px 120px; }
|
||||
|
||||
/* ── Cover ────────────────────────────────────────────────── */
|
||||
.cover {
|
||||
padding: 100px 0 80px;
|
||||
border-bottom: 1px solid rgba(244,234,216,0.78);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
.cover-meta {
|
||||
font-family: var(--sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244,234,216,0.65);
|
||||
}
|
||||
.cover-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
.cover-mark {
|
||||
width: 56px; height: 56px;
|
||||
background: var(--accent);
|
||||
border-radius: 9px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cover-mark span {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 42px;
|
||||
color: var(--brand-text);
|
||||
line-height: 1;
|
||||
transform: translateX(0.02em) translateY(0.03em);
|
||||
display: block;
|
||||
}
|
||||
.cover-wordmark {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 48px;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--brand-text);
|
||||
line-height: 1;
|
||||
}
|
||||
.cover-tagline {
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-size: 20px;
|
||||
color: rgba(244,234,216,0.78);
|
||||
line-height: 1.5;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
/* ── Section chrome ──────────────────────────────────────── */
|
||||
.section {
|
||||
padding: 72px 0 0;
|
||||
}
|
||||
.section-num {
|
||||
font-family: var(--sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.section-title {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
color: var(--brand-text);
|
||||
letter-spacing: 0.01em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.section-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: rgba(244,234,216,0.78);
|
||||
max-width: 560px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.rule {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(244,234,216,0.16);
|
||||
margin-top: 72px;
|
||||
}
|
||||
|
||||
/* ── Logo display ────────────────────────────────────────── */
|
||||
.logo-stage {
|
||||
background: var(--shell-2);
|
||||
border: 1px solid rgba(244,234,216,0.16);
|
||||
border-radius: 10px;
|
||||
padding: 56px 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.logo-stage.light {
|
||||
background: var(--paper);
|
||||
}
|
||||
.lockup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.mark {
|
||||
background: var(--accent);
|
||||
border-radius: calc(var(--sz) * 0.16);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
--sz: 88px;
|
||||
width: var(--sz); height: var(--sz);
|
||||
}
|
||||
.mark .A {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 66px;
|
||||
color: var(--brand-text);
|
||||
line-height: 1;
|
||||
transform: translateX(0.02em) translateY(0.03em);
|
||||
display: block;
|
||||
}
|
||||
.wordmark {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 52px;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--brand-text);
|
||||
line-height: 1;
|
||||
}
|
||||
.wordmark.on-light { color: var(--ink); }
|
||||
|
||||
.logo-stage-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.logo-stage-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244,234,216,0.60);
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* ── Mark-only grid ──────────────────────────────────────── */
|
||||
.mark-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mark-col { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
||||
.mark-sz-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
color: rgba(244,234,216,0.65);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Clear space ─────────────────────────────────────────── */
|
||||
.clearspace-diagram {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
padding: 44px;
|
||||
border: 1px dashed rgba(244,234,216,0.78);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.clearspace-arrows {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cs-label {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244,234,216,0.68);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cs-label.top { top: 14px; left: 50%; transform: translateX(-50%); }
|
||||
.cs-label.bottom { bottom: 14px; left: 50%; transform: translateX(-50%); }
|
||||
.cs-label.left { left: 8px; top: 50%; transform: translateY(-50%) rotate(-90deg); transform-origin: center; }
|
||||
.cs-label.right { right: 6px; top: 50%; transform: translateY(-50%) rotate(90deg); }
|
||||
|
||||
/* ── Don'ts ──────────────────────────────────────────────── */
|
||||
.donts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.dont-card {
|
||||
background: var(--shell-2);
|
||||
border: 1px solid rgba(244,234,216,0.16);
|
||||
border-radius: 8px;
|
||||
padding: 28px 20px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.dont-visual {
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.dont-x {
|
||||
position: absolute;
|
||||
top: -8px; right: -4px;
|
||||
width: 18px; height: 18px;
|
||||
background: #c0392b;
|
||||
border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 10px; font-weight: 700; color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
.dont-label {
|
||||
font-size: 11px;
|
||||
color: rgba(244,234,216,0.72);
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
/* Don't visuals */
|
||||
.dont-stretch {
|
||||
font-family: var(--display); font-weight: 600; font-size: 28px;
|
||||
color: var(--brand-text); letter-spacing: 0.01em;
|
||||
transform: scaleX(1.5);
|
||||
display: inline-block;
|
||||
}
|
||||
.dont-color .mark { --sz: 44px; filter: hue-rotate(180deg); }
|
||||
.dont-color .mark .A { font-size: 33px; }
|
||||
.dont-shadow .mark { --sz: 44px; filter: drop-shadow(0 4px 12px #8d3f30); }
|
||||
.dont-shadow .mark .A { font-size: 33px; }
|
||||
.dont-rotate .lockup { transform: rotate(-15deg); }
|
||||
.dont-rotate .mark { --sz: 44px; }
|
||||
.dont-rotate .mark .A { font-size: 33px; }
|
||||
.dont-rotate .wordmark { font-size: 28px; }
|
||||
.dont-font {
|
||||
font-family: var(--sans); font-weight: 700; font-size: 28px;
|
||||
color: var(--brand-text); letter-spacing: 0.01em;
|
||||
}
|
||||
.dont-bg .mark { --sz: 44px; }
|
||||
.dont-bg .mark .A { font-size: 33px; }
|
||||
.dont-bg-inner {
|
||||
background: repeating-linear-gradient(45deg, #2a3d30, #2a3d30 5px, #1c271f 5px, #1c271f 10px);
|
||||
padding: 10px 14px; border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── Color palette ───────────────────────────────────────── */
|
||||
.palette-group { margin-bottom: 36px; }
|
||||
.palette-group-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244,234,216,0.60);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.swatches {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.swatch { width: 108px; }
|
||||
.swatch-block {
|
||||
height: 64px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.07);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.swatch-name {
|
||||
font-size: 11px;
|
||||
color: rgba(244,234,216,0.88);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.swatch-hex {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 10.5px;
|
||||
color: rgba(244,234,216,0.65);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.swatch-role {
|
||||
font-size: 10px;
|
||||
color: rgba(244,234,216,0.58);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ── Typography ──────────────────────────────────────────── */
|
||||
.type-block {
|
||||
border-top: 1px solid rgba(244,234,216,0.16);
|
||||
padding: 36px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 40px;
|
||||
align-items: start;
|
||||
}
|
||||
.type-meta {}
|
||||
.type-family {
|
||||
font-size: 11px;
|
||||
color: rgba(244,234,216,0.68);
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.type-name {
|
||||
font-size: 14px;
|
||||
color: var(--brand-text);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.type-role {
|
||||
font-size: 11px;
|
||||
color: rgba(244,234,216,0.65);
|
||||
line-height: 1.5;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.type-sample-display {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 52px;
|
||||
color: var(--brand-text);
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.type-sample-display-italic {
|
||||
font-family: var(--display);
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-size: 32px;
|
||||
color: rgba(244,234,216,0.80);
|
||||
line-height: 1.3;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.type-sample-sans {
|
||||
font-family: var(--sans);
|
||||
font-size: 14px;
|
||||
color: var(--brand-text);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.type-sample-sans strong { font-weight: 700; }
|
||||
.type-sample-serif {
|
||||
font-family: var(--serif);
|
||||
font-size: 18px;
|
||||
color: rgba(244,234,216,0.8);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ── Type scale ──────────────────────────────────────────── */
|
||||
.scale-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 20px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid rgba(244,234,216,0.14);
|
||||
}
|
||||
.scale-label {
|
||||
width: 80px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244,234,216,0.60);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.scale-sample {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
color: var(--brand-text);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* ── In context ──────────────────────────────────────────── */
|
||||
.context-app {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(244,234,216,0.16);
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
}
|
||||
.ctx-topbar {
|
||||
background: var(--shell);
|
||||
border-bottom: 2px solid var(--accent);
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.ctx-icon {
|
||||
width: 28px; height: 28px;
|
||||
background: var(--accent);
|
||||
border-radius: 5px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ctx-icon span {
|
||||
font-family: var(--display); font-weight: 600; font-size: 21px;
|
||||
color: var(--brand-text); line-height: 1;
|
||||
transform: translateX(0.02em) translateY(0.03em); display: block;
|
||||
}
|
||||
.ctx-wordmark {
|
||||
font-family: var(--display); font-weight: 600; font-size: 22px;
|
||||
letter-spacing: 0.01em; color: var(--brand-text); line-height: 1;
|
||||
}
|
||||
.ctx-spacer { flex: 1; }
|
||||
.ctx-ghost { width: 80px; height: 24px; background: rgba(244,234,216,0.16); border-radius: 4px; }
|
||||
.ctx-btn {
|
||||
background: linear-gradient(to bottom, color-mix(in srgb, #8d3f30 88%, #fff), #8d3f30);
|
||||
color: #f4ead8; border: none; border-radius: 4px;
|
||||
padding: 7px 20px; font-family: var(--sans); font-size: 12px;
|
||||
font-weight: 550; letter-spacing: 0.08em; text-transform: uppercase; cursor: default;
|
||||
}
|
||||
.ctx-content {
|
||||
background: var(--paper);
|
||||
padding: 24px 20px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.ctx-row {
|
||||
height: 12px; background: var(--paper-2); border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ── File list ───────────────────────────────────────────── */
|
||||
.file-list {
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
.file-row {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 12px 16px;
|
||||
background: var(--shell-2);
|
||||
border: 1px solid rgba(244,234,216,0.16);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.file-icon {
|
||||
width: 32px; height: 32px; flex-shrink: 0;
|
||||
background: rgba(141,63,48,0.15);
|
||||
border-radius: 5px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.file-icon svg { width: 16px; height: 16px; fill: var(--accent); }
|
||||
.file-name { font-size: 12.5px; color: var(--brand-text); flex: 1; font-family: ui-monospace, 'SF Mono', Menlo, monospace; }
|
||||
.file-desc { font-size: 11px; color: rgba(244,234,216,0.65); }
|
||||
|
||||
/* ── Footer ──────────────────────────────────────────────── */
|
||||
.footer {
|
||||
margin-top: 100px;
|
||||
padding-top: 32px;
|
||||
border-top: 1px solid rgba(244,234,216,0.16);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.footer-brand {
|
||||
font-family: var(--display); font-weight: 600; font-size: 16px;
|
||||
color: rgba(244,234,216,0.65); letter-spacing: 0.01em;
|
||||
}
|
||||
.footer-meta {
|
||||
font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
|
||||
color: rgba(244,234,216,0.78);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ══ COVER ═══════════════════════════════════════════════════════════════ -->
|
||||
<div class="cover">
|
||||
<div class="cover-meta">Archivr · Brand Guidelines · v1.0 · 2025</div>
|
||||
<div class="cover-logo">
|
||||
<div class="cover-mark"><span>A</span></div>
|
||||
<div class="cover-wordmark"\>Archivr</div>
|
||||
</div>
|
||||
<div class="cover-tagline">Preserve what matters. Forever.</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ══ 01 THE LOGO ═════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">01</div>
|
||||
<div class="section-title">The Logo</div>
|
||||
<div class="section-desc">The Archivr logo consists of two elements: the mark — a capital A in Cormorant Garamond set inside a rounded square in Terracotta — and the wordmark. Use both together as the primary logo. Never separate them in contexts where the brand is being introduced for the first time.</div>
|
||||
|
||||
<!-- Primary: on dark -->
|
||||
<div style="margin-bottom:12px">
|
||||
<div class="logo-stage">
|
||||
<div class="lockup">
|
||||
<div class="mark"><span class="A">A</span></div>
|
||||
<div class="wordmark"\>Archivr</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo-stage-label">Primary — on dark</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary: on light -->
|
||||
<div>
|
||||
<div class="logo-stage light">
|
||||
<div class="lockup">
|
||||
<div class="mark"><span class="A">A</span></div>
|
||||
<div class="wordmark on-light"\>Archivr</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo-stage-label">Secondary — on light</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 02 MARK ═════════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">02</div>
|
||||
<div class="section-title">The Mark</div>
|
||||
<div class="section-desc">Use the standalone mark when the wordmark would be redundant (favicon, app icon, avatar) or when the space is too small for the full lockup. The mark is recognizable on its own once the brand is established.</div>
|
||||
|
||||
<div class="mark-row">
|
||||
<div class="mark-col">
|
||||
<div class="mark" style="--sz:128px"><span class="A" style="font-size:96px">A</span></div>
|
||||
<div class="mark-sz-label">128 px</div>
|
||||
</div>
|
||||
<div class="mark-col">
|
||||
<div class="mark" style="--sz:64px"><span class="A" style="font-size:48px">A</span></div>
|
||||
<div class="mark-sz-label">64 px</div>
|
||||
</div>
|
||||
<div class="mark-col">
|
||||
<div class="mark" style="--sz:32px; border-radius:5px"><span class="A" style="font-size:24px">A</span></div>
|
||||
<div class="mark-sz-label">32 px</div>
|
||||
</div>
|
||||
<div class="mark-col">
|
||||
<div class="mark" style="--sz:16px; border-radius:3px"><span class="A" style="font-size:12px">A</span></div>
|
||||
<div class="mark-sz-label">16 px</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 03 CLEAR SPACE ══════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">03</div>
|
||||
<div class="section-title">Clear Space</div>
|
||||
<div class="section-desc">Always maintain a minimum clear space equal to half the mark's height (½H) on all sides of the lockup. Never crowd the logo with other elements.</div>
|
||||
|
||||
<div style="display:flex; gap:48px; align-items:flex-start; flex-wrap:wrap">
|
||||
<div>
|
||||
<div class="clearspace-diagram">
|
||||
<div class="cs-label top">½H</div>
|
||||
<div class="cs-label bottom">½H</div>
|
||||
<div class="cs-label left">½H</div>
|
||||
<div class="cs-label right">½H</div>
|
||||
<div class="lockup">
|
||||
<div class="mark" style="--sz:64px"><span class="A" style="font-size:48px">A</span></div>
|
||||
<div class="wordmark" style="font-size:36px"\>Archivr</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding-top:8px">
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:11px;color:rgba(244,234,216,0.68);margin-bottom:6px;letter-spacing:.08em;text-transform:uppercase">Full lockup</div>
|
||||
<div style="font-size:13px;color:rgba(244,234,216,0.88)">Minimum width: <strong style="color:var(--brand-text)">120 px</strong></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:rgba(244,234,216,0.68);margin-bottom:6px;letter-spacing:.08em;text-transform:uppercase">Mark alone</div>
|
||||
<div style="font-size:13px;color:rgba(244,234,216,0.88)">Minimum size: <strong style="color:var(--brand-text)">16 × 16 px</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 04 DON'TS ════════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">04</div>
|
||||
<div class="section-title">Don'ts</div>
|
||||
<div class="section-desc">These are the most common mistakes. All of them dilute or distort the brand.</div>
|
||||
|
||||
<div class="donts-grid">
|
||||
|
||||
<div class="dont-card">
|
||||
<div class="dont-visual">
|
||||
<div style="position:relative">
|
||||
<span class="dont-stretch"\>Archivr</span>
|
||||
<div class="dont-x">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-label">Don't stretch or distort the proportions</div>
|
||||
</div>
|
||||
|
||||
<div class="dont-card">
|
||||
<div class="dont-visual">
|
||||
<div style="position:relative" class="dont-color">
|
||||
<div class="lockup" style="gap:10px">
|
||||
<div class="mark"><span class="A">A</span></div>
|
||||
<div class="wordmark" style="font-size:28px"\>Archivr</div>
|
||||
</div>
|
||||
<div class="dont-x">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-label">Don't change or recolor the logo</div>
|
||||
</div>
|
||||
|
||||
<div class="dont-card">
|
||||
<div class="dont-visual">
|
||||
<div style="position:relative" class="dont-shadow">
|
||||
<div class="lockup" style="gap:10px">
|
||||
<div class="mark"><span class="A">A</span></div>
|
||||
<div class="wordmark" style="font-size:28px"\>Archivr</div>
|
||||
</div>
|
||||
<div class="dont-x">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-label">Don't add shadows, glows, or effects</div>
|
||||
</div>
|
||||
|
||||
<div class="dont-card">
|
||||
<div class="dont-visual">
|
||||
<div style="position:relative" class="dont-rotate">
|
||||
<div class="lockup">
|
||||
<div class="mark"><span class="A">A</span></div>
|
||||
<div class="wordmark"\>Archivr</div>
|
||||
</div>
|
||||
<div class="dont-x" style="top:-18px; right:-12px">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-label">Don't rotate or tilt the logo</div>
|
||||
</div>
|
||||
|
||||
<div class="dont-card">
|
||||
<div class="dont-visual">
|
||||
<div style="position:relative">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div class="mark" style="--sz:44px"><span class="A" style="font-size:33px">A</span></div>
|
||||
<span class="dont-font"\>Archivr</span>
|
||||
</div>
|
||||
<div class="dont-x">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-label">Don't substitute a different typeface for the wordmark</div>
|
||||
</div>
|
||||
|
||||
<div class="dont-card">
|
||||
<div class="dont-visual">
|
||||
<div style="position:relative" class="dont-bg">
|
||||
<div class="dont-bg-inner">
|
||||
<div class="lockup" style="gap:10px">
|
||||
<div class="mark"><span class="A">A</span></div>
|
||||
<div class="wordmark" style="font-size:28px"\>Archivr</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-x">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-label">Don't place on busy or patterned backgrounds</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 05 COLOR ════════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">05</div>
|
||||
<div class="section-title">Color</div>
|
||||
<div class="section-desc">The palette is organized by role. Shell and Paper form the light/dark surface pair. Terracotta is the single brand accent — use it with restraint. Amber is a secondary accent for highlights and metadata. Never introduce colors outside this palette in brand contexts.</div>
|
||||
|
||||
<div class="palette-group">
|
||||
<div class="palette-group-label">Shell & Dark Surfaces</div>
|
||||
<div class="swatches">
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#141d18"></div>
|
||||
<div class="swatch-name">Shell</div>
|
||||
<div class="swatch-hex">#141d18</div>
|
||||
<div class="swatch-role">App topbar, dark surfaces</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#1c271f"></div>
|
||||
<div class="swatch-name">Shell 2</div>
|
||||
<div class="swatch-hex">#1c271f</div>
|
||||
<div class="swatch-role">Elevated dark panels</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#20251f"></div>
|
||||
<div class="swatch-name">Ink</div>
|
||||
<div class="swatch-hex">#20251f</div>
|
||||
<div class="swatch-role">Primary text on light</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="palette-group">
|
||||
<div class="palette-group-label">Paper & Light Surfaces</div>
|
||||
<div class="swatches">
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#f5f0e7; border-color:rgba(0,0,0,.07)"></div>
|
||||
<div class="swatch-name">Paper</div>
|
||||
<div class="swatch-hex">#f5f0e7</div>
|
||||
<div class="swatch-role">Primary content bg</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#e9e1d2; border-color:rgba(0,0,0,.07)"></div>
|
||||
<div class="swatch-name">Paper 2</div>
|
||||
<div class="swatch-hex">#e9e1d2</div>
|
||||
<div class="swatch-role">Rows, alternating bg</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#fffaf0; border-color:rgba(0,0,0,.07)"></div>
|
||||
<div class="swatch-name">Paper 3</div>
|
||||
<div class="swatch-hex">#fffaf0</div>
|
||||
<div class="swatch-role">Elevated panels on light</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#f4ead8; border-color:rgba(0,0,0,.07)"></div>
|
||||
<div class="swatch-name">Brand Text</div>
|
||||
<div class="swatch-hex">#f4ead8</div>
|
||||
<div class="swatch-role">Text on dark & accent</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="palette-group">
|
||||
<div class="palette-group-label">Accents</div>
|
||||
<div class="swatches">
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#8d3f30"></div>
|
||||
<div class="swatch-name">Terracotta</div>
|
||||
<div class="swatch-hex">#8d3f30</div>
|
||||
<div class="swatch-role">Primary brand accent, logo mark, CTAs, focus rings</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#b78342"></div>
|
||||
<div class="swatch-name">Amber</div>
|
||||
<div class="swatch-hex">#b78342</div>
|
||||
<div class="swatch-role">Secondary accent, metadata highlights</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#245f72"></div>
|
||||
<div class="swatch-name">Ocean</div>
|
||||
<div class="swatch-hex">#245f72</div>
|
||||
<div class="swatch-role">Links, informational states</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="palette-group">
|
||||
<div class="palette-group-label">Neutrals & Borders</div>
|
||||
<div class="swatches">
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#6b6f66"></div>
|
||||
<div class="swatch-name">Muted</div>
|
||||
<div class="swatch-hex">#6b6f66</div>
|
||||
<div class="swatch-role">Secondary text, captions</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#8a8d83"></div>
|
||||
<div class="swatch-name">Muted 2</div>
|
||||
<div class="swatch-hex">#8a8d83</div>
|
||||
<div class="swatch-role">Tertiary text, placeholders</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#d2c6b5; border-color:rgba(0,0,0,.07)"></div>
|
||||
<div class="swatch-name">Line</div>
|
||||
<div class="swatch-hex">#d2c6b5</div>
|
||||
<div class="swatch-role">Borders, dividers on light</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-block" style="background:#e5dccd; border-color:rgba(0,0,0,.07)"></div>
|
||||
<div class="swatch-name">Line Soft</div>
|
||||
<div class="swatch-hex">#e5dccd</div>
|
||||
<div class="swatch-role">Subtle dividers, rules</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 06 TYPOGRAPHY ════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">06</div>
|
||||
<div class="section-title">Typography</div>
|
||||
<div class="section-desc">Three typefaces cover every context. Cormorant Garamond is the brand voice — it appears in the logo, headings, and display text. Helvetica Neue handles all interface chrome. Spectral carries long-form reading content.</div>
|
||||
|
||||
<div class="type-block">
|
||||
<div class="type-meta">
|
||||
<div class="type-family">Display</div>
|
||||
<div class="type-name">Cormorant Garamond</div>
|
||||
<div class="type-role">Logo wordmark · Headings · Display text · Taglines<br><br>Weights: 500, 600<br>Google Fonts: free</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="type-sample-display">Aa Bb Cc<br>Preserve what<br>matters.</div>
|
||||
<div class="type-sample-display-italic">The archive of a lifetime,<br>held with care.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="type-block">
|
||||
<div class="type-meta">
|
||||
<div class="type-family">Interface</div>
|
||||
<div class="type-name">Helvetica Neue</div>
|
||||
<div class="type-role">UI labels · Buttons · Nav · Captions · Body copy<br><br>Weights: 400, 550, 700<br>System font (macOS/iOS)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="type-sample-sans">
|
||||
<strong>The quick brown fox jumps</strong> over the lazy dog.<br>
|
||||
Capture · Archive · Preserve · Retrieve<br>
|
||||
0123456789 & #%@!
|
||||
</div>
|
||||
<div style="margin-top:14px;display:flex;gap:24px;flex-wrap:wrap">
|
||||
<span style="font-family:var(--sans);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:rgba(244,234,216,.5)">Small caps label</span>
|
||||
<span style="font-family:var(--sans);font-size:12px;font-weight:550;color:rgba(244,234,216,0.88)">Button Text</span>
|
||||
<span style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:11.5px;color:rgba(244,234,216,.4)">mono/url/code</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="type-block">
|
||||
<div class="type-meta">
|
||||
<div class="type-family">Reading</div>
|
||||
<div class="type-name">Spectral</div>
|
||||
<div class="type-role">Article body · Long-form content · Reading mode<br><br>Weights: 400, 500<br>Google Fonts: free</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="type-sample-serif">An archive is not a graveyard. It is the opposite — a place where things are kept because they still matter, because the person who saved them believed they would matter again.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type scale -->
|
||||
<div style="margin-top:48px">
|
||||
<div style="font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:rgba(244,234,216,.25);margin-bottom:20px">Display scale (Cormorant Garamond 600)</div>
|
||||
<div class="scale-row">
|
||||
<div class="scale-label">72 px</div>
|
||||
<div class="scale-sample" style="font-size:72px"\>Archivr</div>
|
||||
</div>
|
||||
<div class="scale-row">
|
||||
<div class="scale-label">48 px</div>
|
||||
<div class="scale-sample" style="font-size:48px"\>Archivr</div>
|
||||
</div>
|
||||
<div class="scale-row">
|
||||
<div class="scale-label">36 px</div>
|
||||
<div class="scale-sample" style="font-size:36px">Preserve what matters.</div>
|
||||
</div>
|
||||
<div class="scale-row">
|
||||
<div class="scale-label">24 px</div>
|
||||
<div class="scale-sample" style="font-size:24px">Capture, archive, retrieve. Forever.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 07 IN CONTEXT ════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">07</div>
|
||||
<div class="section-title">In Context</div>
|
||||
<div class="section-desc">The logo as it appears in the application shell. The mark sits at 28 × 28 px in the topbar, framed by a 2 px Terracotta rule at the bottom of the navigation bar.</div>
|
||||
|
||||
<div class="context-app">
|
||||
<div class="ctx-topbar">
|
||||
<div class="ctx-icon"><span>A</span></div>
|
||||
<span class="ctx-wordmark"\>Archivr</span>
|
||||
<div class="ctx-spacer"></div>
|
||||
<div class="ctx-ghost"></div>
|
||||
<div class="ctx-btn">Capture</div>
|
||||
</div>
|
||||
<div class="ctx-content">
|
||||
<div class="ctx-row" style="width:62%"></div>
|
||||
<div class="ctx-row" style="width:88%"></div>
|
||||
<div class="ctx-row" style="width:41%"></div>
|
||||
<div class="ctx-row" style="width:75%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="rule">
|
||||
|
||||
<!-- ══ 08 ASSETS ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="section">
|
||||
<div class="section-num">08</div>
|
||||
<div class="section-title">Asset Files</div>
|
||||
<div class="section-desc">All files live in <code style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:12px;color:rgba(244,234,216,.5)">branding/assets/</code>. SVG files embed Cormorant Garamond via Google Fonts — they require a network connection to render the typeface correctly, or the font installed locally.</div>
|
||||
|
||||
<div class="file-list">
|
||||
<div class="file-row">
|
||||
<div class="file-icon">
|
||||
<svg viewBox="0 0 16 16"><path d="M2 2h12v12H2z" fill-opacity=".2"/><path d="M4 4h8M4 7h8M4 10h5" stroke="currentColor" stroke-width="1.2" fill="none" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="file-name">logo-on-dark.svg</div>
|
||||
<div class="file-desc">Full lockup · cream wordmark · use on dark backgrounds</div>
|
||||
</div>
|
||||
<div class="file-row">
|
||||
<div class="file-icon">
|
||||
<svg viewBox="0 0 16 16"><path d="M2 2h12v12H2z" fill-opacity=".2"/><path d="M4 4h8M4 7h8M4 10h5" stroke="currentColor" stroke-width="1.2" fill="none" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="file-name">logo-on-light.svg</div>
|
||||
<div class="file-desc">Full lockup · dark wordmark · use on light / paper backgrounds</div>
|
||||
</div>
|
||||
<div class="file-row">
|
||||
<div class="file-icon">
|
||||
<svg viewBox="0 0 16 16"><path d="M2 2h12v12H2z" fill-opacity=".2"/><path d="M4 4h8M4 7h8M4 10h5" stroke="currentColor" stroke-width="1.2" fill="none" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="file-name">logo-mark.svg</div>
|
||||
<div class="file-desc">Standalone mark · 48 × 48 · for established contexts, avatars</div>
|
||||
</div>
|
||||
<div class="file-row">
|
||||
<div class="file-icon">
|
||||
<svg viewBox="0 0 16 16"><path d="M2 2h12v12H2z" fill-opacity=".2"/><path d="M4 4h8M4 7h8M4 10h5" stroke="currentColor" stroke-width="1.2" fill="none" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="file-name">favicon.svg</div>
|
||||
<div class="file-desc">Favicon · reference from <link rel="icon"> or convert to .ico</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="footer-brand"\>Archivr</div>
|
||||
<div class="footer-meta">Brand Guidelines v1.0 · Internal Use</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -4,8 +4,6 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Archivr</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 295 KiB |
|
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" role="img" aria-label="Archivr">
|
||||
<defs>
|
||||
<style>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap');</style>
|
||||
</defs>
|
||||
<rect width="48" height="48" rx="7.68" fill="#8d3f30"/>
|
||||
<text x="24" y="24" text-anchor="middle" dominant-baseline="central"
|
||||
dx="0.02em" dy="0.03em"
|
||||
font-family="'Cormorant Garamond', Georgia, serif"
|
||||
font-weight="600" font-size="36" fill="#f4ead8">A</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 548 B |