Start Housecarl: the logo, the wordmark and the skeleton
Housecarl will be a firewall manager for nftables, built the way Jarl and Steward are: housecarld (a daemon with a REST API and web UI), hc (the CLI) and SQLite. - The logo: a carved Nordic doorway in Jarl's blue (artwork/logo.svg), with a small version without the carvings, lighter on dark backgrounds. scripts/logo.py builds the web UI's copies and scripts/banner.py the README banner. - The HOUSECARL wordmark in Jarl's and Steward's angular letters, with new H, O, U and C (assets/brand.js), and the UI's color tokens. - housecarld and hc print their version; the Makefile, version package, AGENTS.md, CHANGELOG and the design sketch (docs/design.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
54beb0bba3
24 files changed
+949
No files matched your search
@@ -0,0 +1,150 @@
|
||||
// Package version holds Housecarl's version, set at build time with
|
||||
//
|
||||
// go build -ldflags "-X housecarl/internal/version.Version=1.2.3 -X housecarl/internal/version.Tag=v1.2.3"
|
||||
//
|
||||
// The Makefile sets Version from the VERSION file, and Tag only when the
|
||||
// build is of that release: HEAD is the tag v<VERSION> and no tracked file
|
||||
// is changed. Only such a build reports a version number. Other builds
|
||||
// are development builds, identified by their commit (which Go records
|
||||
// from git by itself) and by where they stand relative to the last tag
|
||||
// (Describe, from git describe).
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Version is the Housecarl version (the VERSION file).
|
||||
var Version = "0.1.0-dev"
|
||||
|
||||
// Tag is the release tag the build is of ("" when it isn't a release).
|
||||
var Tag = ""
|
||||
|
||||
// Commit is the git commit the build is of. It is read from the build
|
||||
// information when not set at build time.
|
||||
var Commit = ""
|
||||
|
||||
// Describe is 'git describe --tags --long' of the build's commit
|
||||
// ("v0.2.0-rc.1-3-gca0f148b78b6"), set by the Makefile; it says how many
|
||||
// commits a development build is past the last tag.
|
||||
var Describe = ""
|
||||
|
||||
// Repo is Housecarl's source repository.
|
||||
const Repo = "https://gitea.adbraun.lol/adam/housecarl"
|
||||
|
||||
// Build describes what a binary was built from.
|
||||
type Build struct {
|
||||
Version string `json:"version"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
Modified bool `json:"modified,omitempty"` // built with uncommitted changes
|
||||
// Since is the last tag before a development build's commit, and
|
||||
// Ahead how many commits the build is past it (0: the tag's commit,
|
||||
// with uncommitted changes).
|
||||
Since string `json:"since,omitempty"`
|
||||
Ahead int `json:"ahead,omitempty"`
|
||||
// Label is the short form to show: the tag of a release, otherwise
|
||||
// the abbreviated commit (or the version when the commit is unknown).
|
||||
Label string `json:"label"`
|
||||
// URL is the release or commit in the repository.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
var info = sync.OnceValue(func() Build {
|
||||
commit, modified := Commit, false
|
||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
if commit == "" {
|
||||
commit = s.Value
|
||||
}
|
||||
case "vcs.modified":
|
||||
modified = s.Value == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
b := describe(Version, Tag, commit, modified)
|
||||
b.Since, b.Ahead = parseDescribe(Describe, b.Commit)
|
||||
return b
|
||||
})
|
||||
|
||||
// parseDescribe splits git describe --long output, if it is of commit.
|
||||
func parseDescribe(d, commit string) (since string, ahead int) {
|
||||
m := describeRe.FindStringSubmatch(d)
|
||||
if m == nil || commit == "" || !strings.HasPrefix(commit, m[3]) {
|
||||
return "", 0
|
||||
}
|
||||
n, _ := strconv.Atoi(m[2])
|
||||
return m[1], n
|
||||
}
|
||||
|
||||
var describeRe = regexp.MustCompile(`^(v.+)-(\d+)-g([0-9a-f]+)$`)
|
||||
|
||||
func describe(version, tag, commit string, modified bool) Build {
|
||||
b := Build{Version: version, Tag: tag, Commit: commit, Modified: modified}
|
||||
switch {
|
||||
case tag != "":
|
||||
b.Modified = false // the Makefile only sets Tag for clean trees
|
||||
b.Label, b.URL = tag, Repo+"/releases/tag/"+tag
|
||||
case commit != "":
|
||||
b.Label, b.URL = short(commit), Repo+"/commit/"+commit
|
||||
if modified {
|
||||
b.Label += "-modified"
|
||||
}
|
||||
default:
|
||||
b.Label, b.URL = version, Repo
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Info describes this build.
|
||||
func Info() Build { return info() }
|
||||
|
||||
// String is the version for logs and 'version' commands. Only a release
|
||||
// build gives a version ("0.2.0"); any other build says it's a
|
||||
// development build: "dev a1b2c3d4e5f6 (3 commits after v0.2.0-rc.1)".
|
||||
func String() string { return Info().String() }
|
||||
|
||||
func (b Build) String() string {
|
||||
switch {
|
||||
case b.Tag != "":
|
||||
return b.Version
|
||||
case b.Commit == "":
|
||||
// No git information (built from a source archive).
|
||||
return b.Version + " (source build)"
|
||||
}
|
||||
s := "dev " + b.Label
|
||||
switch {
|
||||
case b.Since == "":
|
||||
case b.Ahead == 1:
|
||||
s += " (1 commit after " + b.Since + ")"
|
||||
case b.Ahead > 1:
|
||||
s += " (" + strconv.Itoa(b.Ahead) + " commits after " + b.Since + ")"
|
||||
case b.Modified:
|
||||
s += " (" + b.Since + " with uncommitted changes)"
|
||||
default:
|
||||
s += " (at " + b.Since + ")"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Short is the version for a User-Agent: the version of a release,
|
||||
// otherwise the label ("a1b2c3d4e5f6").
|
||||
func Short() string {
|
||||
if b := Info(); b.Tag == "" && b.Commit != "" {
|
||||
return b.Label
|
||||
}
|
||||
return Version
|
||||
}
|
||||
|
||||
func short(commit string) string {
|
||||
if len(commit) > 12 {
|
||||
return commit[:12]
|
||||
}
|
||||
return commit
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package version
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDescribe(t *testing.T) {
|
||||
const c = "4d00be5e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c"
|
||||
for _, tc := range []struct {
|
||||
tag, commit string
|
||||
modified bool
|
||||
label, url string
|
||||
}{
|
||||
{"v0.1.0", c, true, "v0.1.0", Repo + "/releases/tag/v0.1.0"},
|
||||
{"", c, false, "4d00be5e1f0a", Repo + "/commit/" + c},
|
||||
{"", c, true, "4d00be5e1f0a-modified", Repo + "/commit/" + c},
|
||||
{"", "", false, "0.1.0", Repo},
|
||||
} {
|
||||
b := describe("0.1.0", tc.tag, tc.commit, tc.modified)
|
||||
if b.Label != tc.label || b.URL != tc.url {
|
||||
t.Errorf("describe(%q, %q, %v) = %q %q, want %q %q", tc.tag, tc.commit, tc.modified, b.Label, b.URL, tc.label, tc.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
const c = "ca0f148b78b6e1f0a9b8c7d6e5f4a3b2c1d0e9f8"
|
||||
build := func(tag, commit string, modified bool, desc string) Build {
|
||||
b := describe("0.2.0-rc.1", tag, commit, modified)
|
||||
b.Since, b.Ahead = parseDescribe(desc, b.Commit)
|
||||
return b
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
b Build
|
||||
want string
|
||||
}{
|
||||
{build("v0.2.0-rc.1", c, false, "v0.2.0-rc.1-0-gca0f148b78b6"), "0.2.0-rc.1"},
|
||||
{build("", c, false, "v0.2.0-rc.1-3-gca0f148b78b6"), "dev ca0f148b78b6 (3 commits after v0.2.0-rc.1)"},
|
||||
{build("", c, true, "v0.2.0-rc.1-1-gca0f148b78b6"), "dev ca0f148b78b6-modified (1 commit after v0.2.0-rc.1)"},
|
||||
{build("", c, true, "v0.2.0-rc.1-0-gca0f148b78b6"), "dev ca0f148b78b6-modified (v0.2.0-rc.1 with uncommitted changes)"},
|
||||
{build("", c, false, "v0.1.0-0-gca0f148b78b6"), "dev ca0f148b78b6 (at v0.1.0)"},
|
||||
{build("", c, false, ""), "dev ca0f148b78b6"},
|
||||
// A describe of another commit (stale ldflags) is ignored.
|
||||
{build("", c, false, "v0.2.0-rc.1-3-gdeadbeef0000"), "dev ca0f148b78b6"},
|
||||
{build("", "", false, ""), "0.2.0-rc.1 (source build)"},
|
||||
} {
|
||||
if got := tc.b.String(); got != tc.want {
|
||||
t.Errorf("String() = %q, want %q", got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// The Housecarl logo and the login page's wordmark, drawn as inline SVG so
|
||||
// they appear together with the page instead of loading afterwards.
|
||||
//
|
||||
// The "HOUSECARL" wordmark: the angular letters of Jarl's and Steward's
|
||||
// wordmarks (A, R and L are Jarl's, S and E Steward's), with an H whose
|
||||
// crossbar rises like the doorway's roof. Drawn as paths so no font is
|
||||
// needed. The login page shows it over a rule with an FW node; the header
|
||||
// shows the letters alone. The letters use the text color and the rule the
|
||||
// accent color.
|
||||
import * as logoData from "./logo.js";
|
||||
|
||||
const NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
function el(tag, attrs, ...children) {
|
||||
const e = document.createElementNS(NS, tag);
|
||||
for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
|
||||
for (const c of children) e.append(c instanceof Node ? c : document.createTextNode(c));
|
||||
return e;
|
||||
}
|
||||
|
||||
// Letters on a 100-unit cap height; counters are cut with evenodd.
|
||||
const letters = [
|
||||
// H: two stems; the crossbar rises to a peak, like the doorway's roof
|
||||
{ x: 0, d: "M0 0 H18 V40 L32 32 L46 40 V0 H64 V100 H46 V58 L32 50 L18 58 V100 H0 Z" },
|
||||
// O: an octagon, with the cut corners of R's bowl
|
||||
{ x: 76, d: "M20 0 H44 L64 20 V80 L44 100 H20 L0 80 V20 Z M24 16 H40 L46 22 V78 L40 84 H24 L18 78 V22 Z" },
|
||||
// U: cut corners at the foot
|
||||
{ x: 152, d: "M0 0 H18 V78 L24 84 H40 L46 78 V0 H64 V80 L44 100 H20 L0 80 Z" },
|
||||
// S and E: Steward's
|
||||
{ x: 228, d: "M10 0 H54 L58 30 L40 16 H18 V41 H48 L58 51 V90 L48 100 H4 L0 70 L18 84 H40 V59 H10 L0 49 V10 Z" },
|
||||
{ x: 298, d: "M0 0 H52 L56 18 H18 V41 H44 V59 H18 V82 H40 L58 70 L54 100 H0 Z" },
|
||||
// C: S's downward barb at the top, E's upswept spike at the foot
|
||||
{ x: 368, d: "M20 0 H56 L60 30 L42 16 H24 L18 22 V78 L24 84 H42 L60 70 L56 100 H20 L0 80 V20 Z" },
|
||||
// A, R and L: Jarl's
|
||||
{ x: 440, d: "M35 0 L46 10 L64 90 L70 100 H50 L44 76 L35 62 L26 76 L20 100 H0 L6 90 L24 10 Z M35 26 L41 40 L35 54 L29 40 Z" },
|
||||
{ x: 522, d: "M0 0 H48 L60 12 V40 L50 50 L66 100 H46 L34 60 H18 V100 H0 Z M18 14 H41 L46 19 V35 L41 46 H18 Z" },
|
||||
{ x: 600, d: "M0 0 H18 V84 H40 L58 70 L54 100 H0 Z" },
|
||||
];
|
||||
|
||||
const W = 658; // the letters' width
|
||||
|
||||
// wordmark returns the login page's wordmark, letters over the rule.
|
||||
// wordmark({ rule: false, height: 19, cls: "brand-name" }) returns just the
|
||||
// letters, height pixels tall.
|
||||
export function wordmark({ rule = true, height, cls = "wordmark" } = {}) {
|
||||
const letterG = el("g", { fill: "currentColor", "fill-rule": "evenodd" },
|
||||
...letters.map((l) => el("path", { d: l.d, transform: `translate(${l.x} 0)` })));
|
||||
if (!rule) {
|
||||
const attrs = { class: cls, viewBox: `-2 -2 ${W + 4} 104`, role: "img", "aria-label": "Housecarl" };
|
||||
if (height) Object.assign(attrs, { height, width: Math.round(height * (W + 4) / 104) });
|
||||
return el("svg", attrs, el("title", null, "Housecarl"), letterG);
|
||||
}
|
||||
const c = W / 2;
|
||||
const ruleG = el("g", { class: "wordmark-rule", transform: "translate(0 118)" },
|
||||
el("path", { d: `M2 18 L${c - 60} 15.6 V20.4 Z M${W - 2} 18 L${c + 60} 15.6 V20.4 Z`, class: "fill" }),
|
||||
el("circle", { cx: c - 52, cy: 18, r: 4.6, class: "line" }),
|
||||
el("path", { d: `M${c - 47.4} 18 H${c - 24} M${c + 24} 18 H${c + 47.4}`, class: "line" }),
|
||||
el("circle", { cx: c, cy: 18, r: 24, class: "line" }),
|
||||
el("circle", { cx: c + 52, cy: 18, r: 4.6, class: "line" }),
|
||||
el("text", { x: c, y: 24.5, "text-anchor": "middle", class: "tag" }, "FW"));
|
||||
return el("svg", { class: cls, viewBox: `-4 -4 ${W + 8} 176`, role: "img", "aria-label": "Housecarl" },
|
||||
el("title", null, "Housecarl"), letterG, ruleG);
|
||||
}
|
||||
|
||||
// logo returns the Housecarl logo (artwork/logo.svg, via scripts/logo.py)
|
||||
// as an SVG of the given size in pixels: the full doorway, or with small
|
||||
// the one without carvings (for sizes under about 64 pixels). Its color
|
||||
// is the .logo-mark rule in style.css.
|
||||
export function logo(size, { small = false, cls = "logo" } = {}) {
|
||||
return el("svg", { class: cls, width: size, height: size, viewBox: logoData.viewBox, "aria-hidden": "true", focusable: "false" },
|
||||
el("path", { d: small ? logoData.small : logoData.full, "fill-rule": "evenodd", class: "logo-mark" }));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// The Housecarl logo, from artwork/logo.svg and artwork/logo-small.svg by
|
||||
// scripts/logo.py; don't edit. brand.js draws it; logo.svg is the small one.
|
||||
export const viewBox = "26.5 120 769 769";
|
||||
export const full = "M406.5 139.5 L256.5 227.5 L256 228.5 L274.5 258.5 L411 179 L547.5 258.5 L566 228.5 L565.5 227.5 L415.5 139.5 Z M404 211.5 L285 279.5 L279 287 L201.5 354 L157 311.5 L157 278.5 L201 252.5 L218 271 L204.5 286 L195.5 280.5 L182 295 L209 324.5 L260 275 L207.5 212 L124.5 260.5 L124.5 334 L152 364 L152 755.5 L124.5 785 L124.5 828.5 L260 828.5 L260 793 L236 766.5 L236 358 L411 255.5 L586 358 L586 766.5 L562 793 L562 828.5 L697.5 828.5 L697.5 785 L670 755.5 L670 364 L697.5 334 L697.5 260.5 L614.5 212 L562 275 L613 324.5 L640 295 L626.5 280.5 L617.5 286 L604 271 L621 252.5 L665 278.5 L665 311.5 L620.5 354 L543 287 L537 279.5 L411 207 Z M182 416.5 L222.5 460 L222.5 486 L195.5 515 L219.5 541 L219.5 551 L210 561.5 L180 529 L180 503 L207 473 L179.5 443.5 L179.5 416.5 Z M642.5 416.5 L642.5 443.5 L615 473 L642 503 L642 529 L612 561.5 L602.5 551 L602.5 541 L626.5 515 L599.5 486 L599.5 460 L640 416.5 Z M209.5 565.5 L218 574.5 L218 648.5 L176.5 603.5 L205.5 568 L208 565 Z M614 565 L616.5 568 L645.5 603.5 L604 648.5 L604 574.5 L612.5 565.5 Z M184 644 L222.5 686 L222.5 699.5 L222 705.5 L180 751.5 L180 723.5 L205 696 L179.5 668.5 L179.5 641.5 Z M642.5 641.5 L642.5 668.5 L617 696 L642 723.5 L642 751.5 L600 705.5 L599.5 699.5 L599.5 686 L638 644 Z M206.5 594 L197 605 L206 615 L215.5 604.5 L206 594.5 Z M616 594.5 L606.5 604.5 L616 615 L625 605 L615.5 594 Z M392.5 297.5 L261 376 L261 757.5 L288 787.5 L288 829 L399.5 829 L399.5 595 L363 555.5 L400 515 L400 295 L399 294 Z M423 294 L422 295 L422 515 L459 555.5 L422.5 595 L422.5 829 L534 829 L534 787.5 L561 757.5 L561 376 L429.5 297.5 Z M372 350 L372 434.5 L359.5 448 L343 430.5 L353 419 L353 395 L319.5 415 L319.5 444 L370 499.5 L319 555 L370.5 611 L354.5 628.5 L287.5 556 L337.5 501.5 L294 454.5 L294 395.5 Z M528 395.5 L528 454.5 L484.5 501.5 L534.5 556 L467.5 628.5 L451.5 611 L503 555 L452 499.5 L502.5 444 L502.5 415 L469 395 L469 419 L479 430.5 L462.5 448 L450 434.5 L450 350 Z M321 617.5 L342 640.5 L320 665 L320 688 L353 724.5 L353 691 L342.5 679.5 L359.5 661.5 L371.5 675 L371 769 L295.5 705.5 L295 643.5 L319.5 617 Z M502.5 617 L527 643.5 L526.5 705.5 L451 769 L450.5 675 L462.5 661.5 L479.5 679.5 L469 691 L469 724.5 L502 688 L502 665 L480 640.5 L501 617.5 Z M409.5 534.5 L391.5 555 L411 576 L430.5 555 L411 533 Z";
|
||||
export const small = "M406.5 139.5 L256.5 227.5 L256 228.5 L274.5 258.5 L411 179 L547.5 258.5 L566 228.5 L565.5 227.5 L415.5 139.5 Z M404 211.5 L285 279.5 L279 287 L201.5 354 L157 311.5 L157 278.5 L201 252.5 L218 271 L204.5 286 L195.5 280.5 L182 295 L209 324.5 L260 275 L207.5 212 L124.5 260.5 L124.5 334 L152 364 L152 755.5 L124.5 785 L124.5 828.5 L260 828.5 L260 793 L236 766.5 L236 358 L411 255.5 L586 358 L586 766.5 L562 793 L562 828.5 L697.5 828.5 L697.5 785 L670 755.5 L670 364 L697.5 334 L697.5 260.5 L614.5 212 L562 275 L613 324.5 L640 295 L626.5 280.5 L617.5 286 L604 271 L621 252.5 L665 278.5 L665 311.5 L620.5 354 L543 287 L537 279.5 L411 207 Z M392.5 297.5 L261 376 L261 757.5 L288 787.5 L288 829 L399.5 829 L399.5 595 L363 555.5 L400 515 L400 295 L399 294 Z M423 294 L422 295 L422 515 L459 555.5 L422.5 595 L422.5 829 L534 829 L534 787.5 L561 757.5 L561 376 L429.5 297.5 Z";
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="26.5 120 769 769">
|
||||
<!-- The Housecarl logo for small sizes: the doorway without its carvings. -->
|
||||
<style>path{fill:#1d5287}@media (prefers-color-scheme:dark){path{fill:#6d9be8}}</style>
|
||||
<path fill-rule="evenodd" d="M406.5 139.5 L256.5 227.5 L256 228.5 L274.5 258.5 L411 179 L547.5 258.5 L566 228.5 L565.5 227.5 L415.5 139.5 Z M404 211.5 L285 279.5 L279 287 L201.5 354 L157 311.5 L157 278.5 L201 252.5 L218 271 L204.5 286 L195.5 280.5 L182 295 L209 324.5 L260 275 L207.5 212 L124.5 260.5 L124.5 334 L152 364 L152 755.5 L124.5 785 L124.5 828.5 L260 828.5 L260 793 L236 766.5 L236 358 L411 255.5 L586 358 L586 766.5 L562 793 L562 828.5 L697.5 828.5 L697.5 785 L670 755.5 L670 364 L697.5 334 L697.5 260.5 L614.5 212 L562 275 L613 324.5 L640 295 L626.5 280.5 L617.5 286 L604 271 L621 252.5 L665 278.5 L665 311.5 L620.5 354 L543 287 L537 279.5 L411 207 Z M392.5 297.5 L261 376 L261 757.5 L288 787.5 L288 829 L399.5 829 L399.5 595 L363 555.5 L400 515 L400 295 L399 294 Z M423 294 L422 295 L422 515 L459 555.5 L422.5 595 L422.5 829 L534 829 L534 787.5 L561 757.5 L561 376 L429.5 297.5 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,79 @@
|
||||
/* Housecarl web UI. Plain CSS, light and dark themes (the same as Jarl's and
|
||||
Steward's). For now only the colors and the brand: the pages come later. */
|
||||
:root {
|
||||
--bg: #f6f7f9;
|
||||
--panel: #ffffff;
|
||||
--text: #1d2330;
|
||||
--muted: #5f6b7d;
|
||||
--hatch: rgba(95, 107, 125, 0.13);
|
||||
--border: #d9dee6;
|
||||
--accent: #2f5fb3;
|
||||
--accent-text: #ffffff;
|
||||
--accent-soft: #e5edfa;
|
||||
--ok: #1e7a46;
|
||||
--ok-soft: #e3f4ea;
|
||||
--warn: #8a5a00;
|
||||
--warn-soft: #fdf2d9;
|
||||
--err: #b3261e;
|
||||
--err-soft: #fbe7e5;
|
||||
--info-soft: #eef1f6;
|
||||
--code-bg: #f0f2f5;
|
||||
--add: #e6f6ea;
|
||||
--add-text: #135c2c;
|
||||
--del: #fde8e7;
|
||||
--del-text: #8c1d18;
|
||||
--hunk: #e8eefb;
|
||||
--shadow: 0 1px 2px rgba(20, 30, 50, 0.08);
|
||||
--radius: 6px;
|
||||
--vc-green: #3d8160; --vc-teal: #2f7c80; --vc-purple: #7a5ba8; --vc-pink: #a8557f;
|
||||
--vc-orange: #a8652f; --vc-yellow: #8a7424; --vc-red: #b0534c; --vc-grey: #6b7280;
|
||||
--mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
color-scheme: light;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #14171c;
|
||||
--panel: #1c2027;
|
||||
--text: #e3e7ee;
|
||||
--muted: #9aa5b5;
|
||||
--hatch: rgba(154, 165, 181, 0.12);
|
||||
--border: #2f3540;
|
||||
--accent: #6d9be8;
|
||||
--accent-text: #0f1520;
|
||||
--accent-soft: #23314a;
|
||||
--ok: #59c287;
|
||||
--ok-soft: #173326;
|
||||
--warn: #e5b450;
|
||||
--warn-soft: #3a2f15;
|
||||
--err: #f08077;
|
||||
--err-soft: #3c1f1d;
|
||||
--info-soft: #232833;
|
||||
--code-bg: #242933;
|
||||
--add: #183a25;
|
||||
--add-text: #9fe3b5;
|
||||
--del: #43201e;
|
||||
--del-text: #f7b2ac;
|
||||
--hunk: #23314a;
|
||||
--shadow: none;
|
||||
--vc-green: #8fcfa9; --vc-teal: #87cdd0; --vc-purple: #bca6e6; --vc-pink: #e6a6c6;
|
||||
--vc-orange: #ebbb8f; --vc-yellow: #e3d08e; --vc-red: #eba29c; --vc-grey: #aab2bf;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #14171c; --panel: #1c2027; --text: #e3e7ee; --muted: #9aa5b5; --border: #2f3540; --hatch: rgba(154, 165, 181, 0.12);
|
||||
--accent: #6d9be8; --accent-text: #0f1520; --accent-soft: #23314a; --ok: #59c287; --ok-soft: #173326;
|
||||
--warn: #e5b450; --warn-soft: #3a2f15; --err: #f08077; --err-soft: #3c1f1d; --info-soft: #232833;
|
||||
--code-bg: #242933; --add: #183a25; --add-text: #9fe3b5; --del: #43201e; --del-text: #f7b2ac; --hunk: #23314a;
|
||||
|
||||
/* The logo's own blue (artwork/logo.svg), lighter on dark backgrounds. */
|
||||
:root { --logo: #1d5287; }
|
||||
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --logo: #6d9be8; } }
|
||||
:root[data-theme="dark"] { --logo: #6d9be8; }
|
||||
.logo-mark { fill: var(--logo); }
|
||||
|
||||
.wordmark { display: block; width: 220px; height: auto; margin: 0 auto; color: var(--text); }
|
||||
.wordmark-rule .fill { fill: var(--accent); }
|
||||
.wordmark-rule .line { fill: none; stroke: var(--accent); stroke-width: 2.6; }
|
||||
.wordmark-rule .tag { fill: var(--accent); font: 700 17px system-ui, sans-serif; letter-spacing: 0.5px; }
|
||||
.login .wordmark { width: 290px; }
|
||||
Reference in new issue
Block a user