From 53c2514a9464cde1d5c6340e4aabf2d9de51dd7f Mon Sep 17 00:00:00 2001 From: cjimti Date: Sun, 26 Apr 2026 17:32:04 -0700 Subject: [PATCH 1/2] feat(docs): adopt txn2 design system for docs site Re-skin the docs site to match the txn2 visual identity used by sister project kubefwd. Token-alignment level adoption: keep MkDocs Material chrome, replace the visual layer via extra.css and a custom homepage template. - new DESIGN.md records adoption decisions and defers to upstream txn2/www DESIGN.md and tokens.json as the source of truth - new docs/overrides/home.html: rail, hero, two flagship cards (CLI demo, Go library demo), 7-row stack list, coda, home-footer - mkdocs.yml: single slate palette, font: false so CSS loads the upstream Google Fonts URL with trimmed axes - main.html: load Fraunces, Instrument Sans, JetBrains Mono; expand OG and Twitter meta - extra.css: full port of kubefwd's design system stylesheet (1,536 lines: tokens, blueprint grid, grain/vignette, rail, hero, flagship, terminal, stack, coda, home-footer, plus a complete Material chrome restyle for inner pages) - 404.html: rewrite with the design voice - index.md: reduce to a stub pointing at home.html --- DESIGN.md | 115 +++ docs/index.md | 102 +-- docs/overrides/404.html | 22 +- docs/overrides/home.html | 381 +++++++++ docs/overrides/main.html | 12 +- docs/stylesheets/extra.css | 1589 +++++++++++++++++++++++++++++++++--- mkdocs.yml | 18 +- 7 files changed, 2011 insertions(+), 228 deletions(-) create mode 100644 DESIGN.md create mode 100644 docs/overrides/home.html diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..221a397 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,115 @@ +--- +version: alpha +spec: https://github.com/google-labs-code/design.md +name: txeh-docs +description: Local design adoption record for the txeh.txn2.com documentation site. References txn2/www DESIGN.md as the canonical visual identity for tokens, typography, components, voice, and accessibility rules. Records only the decisions and MkDocs Material learnings that the canonical does not cover. +upstream: + design: https://github.com/txn2/www/blob/master/DESIGN.md + tokens: https://github.com/txn2/www/blob/master/tokens.json +adoption: token-alignment +stack: + generator: MkDocs + theme: Material for MkDocs + templates: docs/overrides/ + styles: docs/stylesheets/extra.css +--- + +## What is canonical + +The canonical visual identity for txn2 lives in [`txn2/www/DESIGN.md`](https://github.com/txn2/www/blob/master/DESIGN.md) with tokens in [`txn2/www/tokens.json`](https://github.com/txn2/www/blob/master/tokens.json). This file defers to those for everything below. If a value here disagrees with upstream, upstream wins. + +| Concern | Source of truth | +|----------------------|-----------------| +| Color palette | upstream `tokens.json` `color.*` | +| Typography stack | upstream `tokens.json` `font.*` | +| Type scale | upstream `DESIGN.md` Typography table | +| Spacing / measure | upstream `tokens.json` `size.*` | +| Component contracts | upstream `DESIGN.md` Components | +| Voice / copy rules | upstream `DESIGN.md` Voice and Copy | +| Accessibility rules | upstream `DESIGN.md` Do's and Don'ts | +| Mermaid theme | upstream `DESIGN.md` `mcp__card--feature` block | + +Tokens are mirrored as CSS custom properties in `docs/stylesheets/extra.css` `:root`. They are duplicated for runtime use, not as a divergence point. When upstream changes a token, update the value in `extra.css` and ship. + +## Adoption level: token alignment + +Per the upstream downstream contract, three levels are valid: + +1. Reference. Link to upstream, no visual changes. +2. Token alignment. Keep MkDocs Material, re-skin via `extra.css` against upstream tokens. +3. Full re-skin. Replace MkDocs Material with custom layouts. + +txeh runs at **level 2**, matching its sister project kubefwd. The site keeps Material's instant nav, search, sidebar, version selector, code copy, and content extensions. The visual layer is replaced. The homepage is a custom Material template that takes over `block header`, `block container`, and `block footer` for full-bleed treatment. + +## File map + +| Path | Role | +|------|------| +| `mkdocs.yml` | Single dark `slate` palette. `font: false` so CSS loads the upstream Google Fonts URL with trimmed axes. | +| `docs/index.md` | Stub front matter with `template: home.html`. All homepage HTML lives in the template. | +| `docs/overrides/main.html` | Adds the upstream Google Fonts `` plus OG and Twitter meta. Inherited by every page. | +| `docs/overrides/home.html` | Custom homepage template. Overrides `block header` (rail), `block tabs` (empty), `block container` (page--home shell with hero, sections, flagship cards, stack, coda), `block footer` (home-footer). | +| `docs/overrides/404.html` | Restyled not-found page. Inherits `main.html`, uses `.page--home` shell so the rail and footer match. | +| `docs/stylesheets/extra.css` | All design rules. Two halves: homepage components scoped under `.page--home`, and Material chrome restyle for inner pages via `[data-md-color-scheme="slate"]` variable overrides. | + +## Project-specific components + +Components ported from upstream verbatim, with txeh content: + +- `.rail` (replaces Material `.md-header` on the homepage). Brand links to `./`. Live UTC clock in meta. txn2.com link in meta as `part of txn2 ↗`. +- `.hero` with three Fraunces rows (txeh / hosts file / management.). +- `.section`, `.section__index`, `.section__title`. +- `.flagship__card`. Two cards: a CLI demo card (`add`, `remove`, `list`, `show`) and a Go library card (`NewHostsDefault`, `AddHost`, `Save`). Top accent line animates on hover per upstream spec. +- `.terminal`, `.terminal__bar`, `.terminal__body` with `.t-prompt`, `.t-ok`, `.t-mute` classes. The only block with shadow. +- `.stack`, `.stack__row`. Each row links to a real anchor in the docs (cli/, library/, api/, troubleshooting/). +- `.coda` and `.home-footer` (renamed from upstream `.footer` to avoid collision with markdown that uses `class="footer"`; see kubefwd Learning #5). + +Components from upstream **not used** here, with reason: + +- `.mcp__card--feature` and the MCP grid. txeh is a single library, not an MCP catalog. +- The 5-column footer's `sponsors / craig` columns. The txeh home-footer has `about / docs / interfaces / code / txn2 / org` columns instead, since this site is project-scoped. + +Custom additions specific to txeh: + +- `home-footer__col--meta` includes a `txn2 / org` panel that backlinks to txn2.com explicitly. Per the org-wide rule that every sister project must clearly link home. +- The CLI flagship card's terminal demo uses real `txeh add`, `txeh list ip`, and `txeh remove host` invocations against `127.0.0.1`. The library card shows the `NewHostsDefault` / `AddHost` / `Save` path. + +## MkDocs Material learnings + +The Material learnings list applies to every MkDocs Material project re-skinned to the txn2 identity. txeh inherits them all from kubefwd. Read the full set in [`txn2/kubefwd/DESIGN.md`](https://github.com/txn2/kubefwd/blob/master/DESIGN.md) "MkDocs Material learnings". Brief summary so this file remains useful in isolation: + +1. Override the homepage via a separate template, not via CSS hacks. +2. Re-skin inner pages via Material variable overrides on `[data-md-color-scheme="slate"]`. +3. `font: false` to load fonts directly from CSS. +4. Scope every homepage component class under `.page--home`. +5. Rename `.footer` to `.home-footer` to avoid collision. +6. `h3` and `h4` are technical reference, not display type. Switch them to Instrument Sans bold; flip any heading containing inline code to JetBrains Mono via `:has(code)` with a `@supports not selector(:has(*))` fallback. +7. Tabbed content nests boxes by default. Strip `.tabbed-set` background and border, keep only the label underline. +8. Mermaid via Material's `--md-mermaid-*` CSS variables, not via separate init. +9. Guard inline scripts against `navigation.instant` rehydration. The live UTC clock uses a `window.__txehClock` sentinel. +10. Drop the light/dark toggle. Single `scheme: slate`. +11. Atmospheric overlays at low z-index (grain and vignette at z-index 1, below rail at z-50). +12. Hugo-only token compilation does not exist in MkDocs. Token sync is a manual edit to `extra.css`. + +## Voice and copy + +Defers to upstream. Briefly: + +- No em-dashes (U+2014) or en-dashes (U+2013) anywhere, including code comments and template comments. Use commas, periods, colons, parentheses, slashes, hyphens. +- No AI-tell vocabulary: `seamless`, `leverage`, `comprehensive`, `robust`, `delve`, `unleash`, `elevate`, `embark`, `tapestry`, `not just X but Y`, `as an AI`, `let me X`. +- Sentence case for body. Lowercase for rail and label text. Title case rare. +- Section indices: `§ 01 / title` with slash, never an em-dash. +- Year ranges use a hyphen: `2018-2026`. +- Verify before commit: `grep -RE "—|–" docs/ mkdocs.yml`. + +## Updating + +When the upstream `txn2/www/DESIGN.md` or `tokens.json` changes: + +1. Read the upstream diff. Identify which tokens, components, or rules changed. +2. Update the matching CSS variables in `docs/stylesheets/extra.css` `:root`. +3. If a component contract changed (padding, border, hover behavior), update the homepage template in `docs/overrides/home.html` and the matching CSS rules. +4. Update this file's File map / Project-specific components sections if a new component is added or removed. +5. Run `mkdocs build --strict` and verify in the browser before committing. Verify the home page hero, flagship cards, terminal, stack, coda, and home-footer. Verify an inner page (`/cli/`, `/library/`) still inherits the look. + +Keep this file thin. If a section grows past 30 lines, ask whether it belongs upstream instead. diff --git a/docs/index.md b/docs/index.md index bdab22a..a329882 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,102 +1,10 @@ --- +title: txeh +template: home.html hide: + - navigation - toc + - footer --- -
- txeh -
- -# txn2/txeh - -/etc/hosts management as a Go library and CLI utility. Programmatic and command-line access to add, remove, and query hostname-to-IP mappings. Thread-safe, cross-platform, and built to support tools like [kubefwd](https://github.com/txn2/kubefwd). - -[Get Started](getting-started.md){ .md-button .md-button--primary } -[View on GitHub](https://github.com/txn2/txeh){ .md-button } - -## Two Ways to Use - -
- -- :material-console:{ .lg .middle } **Use the CLI** - - --- - - Manage `/etc/hosts` entries from the command line. - - - Add, remove, list hostnames - - CIDR range operations - - Dry run mode for previewing changes - - [:octicons-arrow-right-24: CLI Reference](cli.md) - -- :material-code-braces:{ .lg .middle } **Use the Go Library** - - --- - - Import into your Go application for programmatic hosts file management. - - - Thread-safe with mutex locking - - In-memory mode from strings - - Inline comment support - - [:octicons-arrow-right-24: Library docs](library.md) - -
- -## Core Features - -
- -- :material-lock-outline:{ .lg .middle } **Thread-Safe** - - --- - - All public methods use mutex locking for safe concurrent access from multiple goroutines. - -- :material-ip-network:{ .lg .middle } **IPv4 & IPv6** - - --- - - Full support for both address families with family-specific lookups. - -- :material-select-group:{ .lg .middle } **CIDR Operations** - - --- - - Bulk add and remove hosts by CIDR range. List all entries in a network. - -- :material-monitor:{ .lg .middle } **Cross-Platform** - - --- - - Linux, macOS, and Windows. Auto-detects the system hosts file location. - -
- -## Quick Install - -=== "Homebrew" - - ```bash - brew install txn2/tap/txeh - ``` - -=== "Go Install" - - ```bash - go install github.com/txn2/txeh/txeh@latest - ``` - -=== "Go Library" - - ```bash - go get github.com/txn2/txeh - ``` - ---- - -[![CI](https://github.com/txn2/txeh/actions/workflows/ci.yml/badge.svg)](https://github.com/txn2/txeh/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/txn2/txeh/branch/master/graph/badge.svg)](https://codecov.io/gh/txn2/txeh) -[![Go Report Card](https://goreportcard.com/badge/github.com/txn2/txeh)](https://goreportcard.com/report/github.com/txn2/txeh) -[![Go Reference](https://pkg.go.dev/badge/github.com/txn2/txeh.svg)](https://pkg.go.dev/github.com/txn2/txeh) + diff --git a/docs/overrides/404.html b/docs/overrides/404.html index 18b72cb..ea01130 100644 --- a/docs/overrides/404.html +++ b/docs/overrides/404.html @@ -3,22 +3,22 @@ {% block content %}
-

Page Not Found

-

The page you're looking for doesn't exist or has been moved.

+

404 / not found

+

That page does not exist, or has moved.

-

Quick Links

+

Quick links

-

Or use the search box above to find what you're looking for.

+

Or use the search box above to find what you are looking for.

-

View on GitHub

+

view on github

{% endblock %} diff --git a/docs/overrides/home.html b/docs/overrides/home.html new file mode 100644 index 0000000..95793a4 --- /dev/null +++ b/docs/overrides/home.html @@ -0,0 +1,381 @@ +{% extends "main.html" %} + +{# Mark the body so CSS can target the homepage layout. #} +{% block htmltitle %} + {{ super() }} +{% endblock %} + +{% block site_meta %} + {{ super() }} + +{% endblock %} + +{# Replace Material's header with the txn2 rail. #} +{% block header %} +
+ + + + txeh + + /etc/hosts management for go + +
+ v1.x + + ·· UTC + + part of txn2 ↗ +
+ +
+{% endblock %} + +{# No nav tabs on the homepage. #} +{% block tabs %}{% endblock %} + +{# No announce. #} +{% block announce %}{% endblock %} + +{# Replace the entire content area with the full-bleed homepage. #} +{% block container %} +
+ + {# BLUEPRINT GRID OVERLAY #} + + + {# HERO #} +
+
+

project / txeh

+

org / txn2

+

est. 2018

+

pkg.go.dev ↗

+

apache 2.0 · cross platform

+
+ +

+ + txeh + v1.x · go + + + /etc/hosts + + + management. + +

+ +
+

+ txeh manages the /etc/hosts file from Go and from the shell. Add, remove, list, and query hostname-to-IP mappings without hand-editing. Mutex-protected for safe concurrent use, comment-aware so entries from different sources stay separable, and cross-platform across Linux, macOS, and Windows. The library powers kubefwd's per-service hostname resolution and ships as a standalone CLI. +

+

+ IPv4 and IPv6, CIDR-based bulk add and remove, dry-run preview, optional DNS cache flush after writes, and an in-memory mode that operates on raw text instead of a file. Apache 2.0, originally built to support kubefwd for Kubernetes service forwarding. +

+
+ + +
+ + {# FLAGSHIP / PRIMARY DEMO #} +
+
+

§ 01 / install · run

+

+ Install. Then use it. +

+

+ Install with brew, go install, or grab a release binary. Run the CLI with sudo so txeh can write /etc/hosts. Or import the Go library and manage hosts from a service, daemon, or test fixture. +

+
+ +
+
+
+

CLI-001 · go · cli reference ↗

+

cli mode

+

Add, remove, list, and show entries from the shell.

+
+
+

One binary, no daemons. Add a single hostname, a comma-separated list, or remove a whole CIDR range. Use --dryrun to preview changes, --comment to tag entries by source, and --flush to refresh the DNS cache after writes.

+
+
+
+
+ + ~ / txeh +
+
$ brew install txn2/tap/txeh
+$ sudo txeh add 127.0.0.1 myapp.local
+  + 127.0.0.1 myapp.local
+
+$ txeh list ip 127.0.0.1
+  localhost myapp.local
+
+$ sudo txeh remove host myapp.local --dryrun
+# preview only, no write
+
+
+ +
+ +
+
+

LIB-002 · go · library docs ↗

+

go library

+

Programmatic hosts file management. Thread-safe.

+
+
+

NewHostsDefault() finds the system hosts file and returns a mutex-protected handle. Call AddHost, RemoveHost, or RemoveCIDRs from any goroutine and Save() to write. Or pass RawText to operate on a string in-memory.

+
+
+
+
+ + ~ / library +
+
$ go get github.com/txn2/txeh
+
+// main.go
+hosts, _ := txeh.NewHostsDefault()
+hosts.AddHost("127.0.0.1", "myapp.local")
+hosts.AddHostWithComment(
+    "10.0.0.1", "db", "staging",
+)
+_ = hosts.Save()
+
+
+ +
+
+
+ + {# WHAT TXEH DOES / STACK LIST #} +
+
+

§ 02 / what it does

+

+ Hosts file as data. Not a string to grep. +

+

+ txeh parses the hosts file into a structured representation, mutates it through typed methods, and renders it back without losing comments or formatting. +

+
+ +
    +
  1. + 001 +
    + add and remove by host, ip, or cidr +

    Single-host or bulk operations. AddHost, AddHosts, RemoveHost, RemoveAddress, RemoveCIDRs. Operations are idempotent; re-adding a hostname is a no-op.

    +
    + core + +
  2. +
  3. + 002 +
    + thread-safe by default +

    All public methods take an internal mutex. Daemons, controllers, and tools that mutate hosts from multiple goroutines (kubefwd among them) can call them directly without external locking.

    +
    + runtime + +
  4. +
  5. + 003 +
    + inline comments preserved +

    AddHostWithComment tags entries with a source string. RemoveByComment removes everything tagged with that source. The library re-renders the file without losing any comment or blank line.

    +
    + format + +
  6. +
  7. + 004 +
    + query by host, ip, or comment +

    ListAddressesByHost, ListHostsByIP, ListHostsByComment, ListHostsByCIDR. Read-only inspection without parsing or rewriting.

    +
    + query + +
  8. +
  9. + 005 +
    + dns cache flush +

    --flush on the CLI or TXEH_AUTO_FLUSH=1 in the environment runs the OS-native flush command after writes. dscacheutil on macOS, resolvectl on Linux, ipconfig /flushdns on Windows.

    +
    + dns + +
  10. +
  11. + 006 +
    + in-memory mode for tests +

    Pass RawText to NewHosts and operate on a string. Save() errors as expected (no path); RenderHostsFile() returns the mutated text. Useful in unit tests and ephemeral fixtures.

    +
    + testing + +
  12. +
  13. + ··· +
    + ipv4 and ipv6, custom paths, dry-run, version stamping +

    Full type and method reference in the API docs.

    +
    + + more + +
  14. +
+
+ + {# CODA / sister projects, context #} +
+
+

// open source

+

+ txeh is one of several open source components by Craig Johnston, sponsored by Deasil Works, Inc. and Plexara. Released under the Apache 2.0 license. Originally built to support kubefwd for Kubernetes service forwarding. +

+ +
+
+ +
+{% endblock %} + +{# Custom homepage footer replaces Material's. Class names prefixed + home-footer to avoid global collision with markdown class="footer". #} +{% block footer %} + +{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock %} diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 0cd8b4a..c0cab4d 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,16 +1,20 @@ {% extends "base.html" %} {% block extrahead %} - + + + + - + + - - + + {% endblock %} diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 5d72c11..901553c 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,152 +1,1537 @@ -/* txeh Docs - Clean Design (txn2 color scheme) */ +/* ──────────────────────────────────────────────────────────────── + txeh / docs · txn2 design system + Ported from sister project kubefwd's extra.css, which itself ports + txn2/www/static/css/site.css plus the MkDocs Material chrome re-skin. + See DESIGN.md for the canonical source and update workflow. + ─────────────────────────────────────────────────────────────── */ :root { - --tx-green: #5D7A6B; - --tx-green-dark: #4a6356; - --tx-green-light: #6e8a7b; - --tx-text: #2d3748; - --tx-text-muted: #718096; - --tx-bg: #f7fafc; - --tx-border: #e2e8f0; + --ink: #0B0B09; + --ink-2: #131310; + --ink-3: #1B1A15; + --rule: #2A2922; + --rule-2: #3A3830; + + --paper: #ECE3CE; + --paper-dim: #C9C0AB; + --mute: #968F80; + --mute-2: #5A554B; + + --signal: #FF5A1F; + --signal-2: #FF8A4C; + --acid: #E1FF6E; + --cool: #6FE1FF; + + --serif: "Fraunces", "Times New Roman", serif; + --sans: "Instrument Sans", -apple-system, "Helvetica Neue", sans-serif; + --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; + + --measure: 64ch; + --rail-h: 56px; + --gutter: clamp(20px, 4vw, 64px); } +/* ────────────────────────────────────────────────────────────── + MATERIAL VARIABLE OVERRIDES (slate scheme = our dark palette) + These re-skin every inner doc page automatically. + ────────────────────────────────────────────────────────────── */ + [data-md-color-scheme="slate"] { - --tx-green: #5D7A6B; - --tx-green-dark: #4a6356; - --tx-green-light: #6e8a7b; - --tx-text: #e2e8f0; - --tx-text-muted: #a0aec0; - --tx-bg: #1a202c; - --tx-border: #2d3748; -} - -/* ===== COLOR VARIABLES - LIGHT MODE ===== */ -:root, -[data-md-color-scheme="default"] { - --md-primary-fg-color: #5D7A6B; - --md-primary-fg-color--light: #6e8a7b; - --md-primary-fg-color--dark: #4a6356; - --md-accent-fg-color: #5D7A6B; - --md-typeset-a-color: #5D7A6B; -} - -/* ===== COLOR VARIABLES - DARK MODE ===== */ -[data-md-color-scheme="slate"] { - --md-primary-fg-color: #5D7A6B; - --md-primary-fg-color--light: #6e8a7b; - --md-primary-fg-color--dark: #4a6356; - --md-accent-fg-color: #6e8a7b; - --md-typeset-a-color: #6e8a7b; - --md-default-bg-color: #1a202c; - --md-default-bg-color--light: #2d3748; - --md-default-bg-color--lighter: #4a5568; - --md-default-bg-color--lightest: #718096; - --md-code-bg-color: #171923; -} - -/* ===== HEADER ===== */ -.md-header { - background: var(--tx-green); + --md-default-bg-color: var(--ink); + --md-default-bg-color--light: var(--ink-2); + --md-default-bg-color--lighter: var(--ink-3); + --md-default-bg-color--lightest: rgba(255,255,255,0.02); + + --md-default-fg-color: var(--paper); + --md-default-fg-color--light: var(--paper-dim); + --md-default-fg-color--lighter: var(--mute); + --md-default-fg-color--lightest: var(--mute-2); + + --md-primary-fg-color: var(--ink-2); + --md-primary-fg-color--light: var(--ink-3); + --md-primary-fg-color--dark: var(--ink); + --md-primary-bg-color: var(--paper); + --md-primary-bg-color--light: var(--paper-dim); + + --md-accent-fg-color: var(--signal); + --md-accent-fg-color--transparent: rgba(255,90,31,0.10); + --md-accent-bg-color: var(--ink); + --md-accent-bg-color--light: var(--ink-2); + + --md-typeset-color: var(--paper); + --md-typeset-a-color: var(--signal); + --md-typeset-mark-color: rgba(255,90,31,0.25); + --md-typeset-kbd-color: var(--ink-3); + --md-typeset-kbd-accent-color: var(--rule-2); + --md-typeset-kbd-border-color: var(--rule); + + --md-code-bg-color: var(--ink-2); + --md-code-fg-color: var(--paper); + --md-code-hl-color: rgba(255,90,31,0.15); + --md-code-hl-color--light: rgba(255,90,31,0.08); + --md-code-hl-name-color: var(--paper); + --md-code-hl-keyword-color: var(--signal); + --md-code-hl-string-color: var(--acid); + --md-code-hl-number-color: var(--cool); + --md-code-hl-comment-color: var(--mute); + --md-code-hl-special-color: var(--signal-2); + --md-code-hl-function-color: var(--paper); + --md-code-hl-constant-color: var(--cool); + --md-code-hl-operator-color: var(--paper-dim); + --md-code-hl-punctuation-color: var(--paper-dim); + + --md-text-font: var(--sans); + --md-code-font: var(--mono); + + --md-footer-bg-color: var(--ink-2); + --md-footer-bg-color--dark: var(--ink); + --md-footer-fg-color: var(--paper); + --md-footer-fg-color--light: var(--paper-dim); + --md-footer-fg-color--lighter: var(--mute); + + --md-shadow-z1: 0 1px 0 rgba(255,255,255,0.02); + --md-shadow-z2: 0 1px 0 rgba(255,255,255,0.02); + --md-shadow-z3: 0 1px 0 rgba(255,255,255,0.02); + + /* Mermaid theme variables consumed by MkDocs Material's + Mermaid integration. Mirrors the txn2 DESIGN.md Mermaid block. */ + --md-mermaid-font-family: var(--mono); + --md-mermaid-edge-color: var(--mute); + --md-mermaid-node-bg-color: var(--ink-3); + --md-mermaid-node-fg-color: var(--paper); + --md-mermaid-label-bg-color: var(--ink-3); + --md-mermaid-label-fg-color: var(--paper); + --md-mermaid-sequence-actor-bg-color: var(--ink-3); + --md-mermaid-sequence-actor-fg-color: var(--paper); + --md-mermaid-sequence-actor-border-color: var(--rule-2); + --md-mermaid-sequence-actor-line-color: var(--rule-2); + --md-mermaid-sequence-actorman-bg-color: var(--ink-3); + --md-mermaid-sequence-actorman-line-color: var(--mute); + --md-mermaid-sequence-box-bg-color: var(--ink-2); + --md-mermaid-sequence-box-fg-color: var(--paper); + --md-mermaid-sequence-label-bg-color: var(--ink-2); + --md-mermaid-sequence-label-fg-color: var(--mute); + --md-mermaid-sequence-loop-bg-color: var(--ink-2); + --md-mermaid-sequence-loop-fg-color: var(--paper); + --md-mermaid-sequence-loop-border-color: var(--rule-2); + --md-mermaid-sequence-message-fg-color: var(--paper-dim); + --md-mermaid-sequence-message-line-color: var(--mute); + --md-mermaid-sequence-note-bg-color: var(--ink-2); + --md-mermaid-sequence-note-fg-color: var(--paper); + --md-mermaid-sequence-note-border-color: var(--signal); + --md-mermaid-sequence-number-bg-color: var(--signal); + --md-mermaid-sequence-number-fg-color: var(--ink); } -[data-md-color-scheme="slate"] .md-header { - background: #2d3d35; +/* ────────────────────────────────────────────────────────────── + GLOBAL BASE + ────────────────────────────────────────────────────────────── */ + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +html, body, .md-container, .md-main { + background: var(--ink); } -/* ===== TABS ===== */ -.md-tabs { - background: var(--tx-green-dark); +body { + font-family: var(--sans); + color: var(--paper); + font-feature-settings: "ss01", "ss02", "cv11"; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +::selection { background: var(--signal); color: var(--ink); } + +:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 3px; + border-radius: 2px; } +:focus:not(:focus-visible) { outline: none; } + +img { max-width: 100%; display: block; } + +em.serif { font-family: var(--serif); font-style: italic; font-weight: 400; } -[data-md-color-scheme="slate"] .md-tabs { - background: #1a202c; +/* Skip-to-content link */ +.skiplink { + position: absolute; + top: -100px; + left: 12px; + z-index: 200; + background: var(--signal); + color: var(--ink); + padding: 10px 16px; + border-radius: 2px; + font-family: var(--mono); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 700; + transition: top .15s ease; +} +.skiplink:focus, +.skiplink:focus-visible { + top: 12px; + color: var(--ink); + outline: 2px solid var(--paper); + outline-offset: 2px; } -/* ===== TYPOGRAPHY ===== */ -.md-typeset { - line-height: 1.7; +/* ────────────────────────────────────────────────────────────── + FILM GRAIN + VIGNETTE / fixed viewport overlays, page-wide + z-index 1: above the page background, below the rail (z 50), + skiplink (z 200), and any interactive Material chrome. The + grain uses overlay blend so it stays subtle even over text; + the vignette only dims the corners. + ────────────────────────────────────────────────────────────── */ + +body::before, +body::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; +} +body::before { + opacity: .08; + mix-blend-mode: overlay; + background-image: url("data:image/svg+xml;utf8,"); +} +body::after { + background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.45) 100%); +} + +/* Subtle warm tint behind the canvas, like the reference. */ +.md-container, +.page--home { + background-image: + radial-gradient(circle at 18% 6%, rgba(255,90,31,0.05), transparent 38%), + radial-gradient(circle at 92% 84%, rgba(225,255,110,0.025), transparent 42%), + linear-gradient(180deg, var(--ink) 0%, var(--ink-2) 100%); +} + +/* ────────────────────────────────────────────────────────────── + HOMEPAGE PAGE WRAPPER & BLUEPRINT + ────────────────────────────────────────────────────────────── */ + +.page--home { + position: relative; + z-index: 1; + display: block; + width: 100%; + margin: 0; + padding: 0; + color: var(--paper); +} + +.blueprint { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; +} +.blueprint__grid { + position: absolute; inset: 0; + background-image: + linear-gradient(to right, rgba(236,227,206,0.04) 1px, transparent 1px), + linear-gradient(to bottom, rgba(236,227,206,0.04) 1px, transparent 1px); + background-size: 64px 64px; + -webkit-mask-image: linear-gradient(180deg, transparent, black 12%, black 88%, transparent); + mask-image: linear-gradient(180deg, transparent, black 12%, black 88%, transparent); +} +.blueprint__crosshair { position: absolute; width: 28px; height: 28px; } +.blueprint__crosshair span { position: absolute; background: var(--mute-2); } +.blueprint__crosshair span:nth-child(1) { top: 50%; left: 0; right: 0; height: 1px; } +.blueprint__crosshair span:nth-child(2) { left: 50%; top: 0; bottom: 0; width: 1px; } +.blueprint__crosshair--tl { top: 24px; left: 24px; } +.blueprint__crosshair--tr { top: 24px; right: 24px; } +.blueprint__crosshair--bl { bottom: 24px; left: 24px; } +.blueprint__crosshair--br { bottom: 24px; right: 24px; } + +/* ────────────────────────────────────────────────────────────── + TOP RAIL (replaces Material header on homepage) + ────────────────────────────────────────────────────────────── */ + +.md-header.rail, +header.rail { + position: sticky; + top: 0; + z-index: 50; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 24px; + padding: 14px var(--gutter); + background: rgba(11,11,9,0.72); + backdrop-filter: blur(14px) saturate(140%); + -webkit-backdrop-filter: blur(14px) saturate(140%); + border-bottom: 1px solid var(--rule); + font-family: var(--mono); + font-size: 12px; + letter-spacing: 0.02em; + text-transform: lowercase; + box-shadow: none; + height: auto; + color: var(--paper); +} +header.rail a { color: inherit; text-decoration: none; } +a.rail__brand, +.rail__brand { + display: flex; align-items: center; gap: 10px; + text-decoration: none; + color: inherit; +} +a.rail__brand:hover .rail__name { color: var(--signal); } +a.rail__brand:hover .rail__sub { color: var(--paper-dim); } +a.rail__brand:focus-visible { outline-offset: 4px; } +.rail__nav a[aria-current="page"] { color: var(--signal); font-weight: 700; } +.rail__mark { + display: inline-block; + color: var(--signal); + font-size: 18px; + transform: translateY(-1px); + animation: spin 24s linear infinite; +} +.rail__name { + font-family: var(--serif); + font-style: italic; + font-size: 22px; + text-transform: lowercase; + letter-spacing: -0.01em; + color: var(--paper); + font-weight: 400; +} +.rail__sep { color: var(--rule-2); } +.rail__sub { color: var(--mute); } + +.rail__meta { + justify-self: center; + color: var(--mute); + display: flex; gap: 12px; align-items: center; +} +.rail__meta .rail__dot { color: var(--rule-2); font-size: 8px; } +.rail__rel { + color: var(--signal); + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.rail__org { + color: var(--paper-dim); + border-bottom: 1px solid transparent; + transition: color .2s, border-color .2s; +} +.rail__org em.serif { + color: var(--paper); + font-style: italic; + font-family: var(--serif); +} +.rail__org:hover { color: var(--signal); border-bottom-color: var(--signal); } +.rail__org:hover em.serif { color: var(--signal); } + +.rail__nav { + justify-self: end; + display: flex; gap: 22px; align-items: center; +} +.rail__nav a { color: var(--paper-dim); transition: color .2s; } +.rail__nav a:hover { color: var(--signal); } +.rail__cta { + color: var(--ink) !important; + background: var(--signal); + padding: 6px 12px; + border-radius: 2px; + font-weight: 700; +} +.rail__cta:hover { background: var(--signal-2) !important; color: var(--ink) !important; } + +@keyframes spin { from { transform: rotate(0deg) translateY(-1px); } to { transform: rotate(360deg) translateY(-1px); } } + +@media (max-width: 1080px) { + header.rail { grid-template-columns: 1fr auto; gap: 16px; } + .rail__meta { display: none; } } +@media (max-width: 720px) { + .rail__sep, .rail__sub { display: none; } + .rail__nav { gap: 16px; } +} +@media (max-width: 540px) { + header.rail { padding: 12px 16px; gap: 10px; } + .rail__brand { gap: 6px; min-width: 0; } + .rail__name { font-size: 18px; } + .rail__mark { font-size: 16px; } + .rail__nav { gap: 12px; font-size: 11px; } + .rail__cta { padding: 5px 8px; } +} +@media (max-width: 380px) { + .rail__nav a:not(.rail__cta) { display: none; } +} + +/* ────────────────────────────────────────────────────────────── + HERO + All rules scoped under .page--home so component classes never + leak into Material-rendered inner pages. + ────────────────────────────────────────────────────────────── */ + +.page--home .hero { + position: relative; + padding: clamp(60px, 12vh, 140px) var(--gutter) clamp(80px, 14vh, 160px); + max-width: 1500px; + margin: 0 auto; +} + +.page--home .hero__stamp { + position: absolute; + top: clamp(40px, 8vh, 90px); + right: var(--gutter); + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--mute); + text-align: right; + border-right: 1px solid var(--signal); + padding-right: 14px; + animation: rise 1.1s .1s both ease-out; +} +.page--home .hero__stamp__line { line-height: 1.7; } +.page--home .hero__stamp__line--ok { color: var(--signal); } -[data-md-color-scheme="default"] .md-typeset { - color: #2d3748; +.page--home .hero__display { + font-family: var(--serif); + font-weight: 300; + font-size: clamp(60px, 12.5vw, 220px); + line-height: 0.92; + letter-spacing: -0.04em; + margin-bottom: 56px; + font-variation-settings: "opsz" 144; +} +.page--home .hero__row { + display: flex; align-items: baseline; gap: clamp(12px, 2vw, 36px); + flex-wrap: wrap; +} +.page--home .hero__row--1 { animation: rise 1.1s .25s both ease-out; } +.page--home .hero__row--2 { + padding-left: clamp(40px, 14vw, 240px); + animation: rise 1.1s .4s both ease-out; +} +.page--home .hero__row--3 { + padding-left: clamp(20px, 6vw, 100px); + animation: rise 1.1s .55s both ease-out; +} +.page--home .hero__row em.serif { + font-style: italic; + color: var(--paper); + font-variation-settings: "opsz" 144; +} +.page--home .outline { + -webkit-text-stroke: 1.5px var(--paper); + color: transparent; + font-style: normal; + font-weight: 400; + font-family: var(--serif); +} +.page--home .hero__chip { + font-family: var(--mono); + font-size: 13px; + letter-spacing: 0.04em; + color: var(--mute); + background: var(--ink-3); + border: 1px solid var(--rule); + padding: 6px 12px; + border-radius: 2px; + text-transform: uppercase; + align-self: center; + transform: translateY(-12px); + white-space: nowrap; } -/* ===== CONTENT WIDTH ===== */ -.md-content__inner { - max-width: 52rem; +.page--home .hero__intro { + display: grid; + grid-template-columns: 1fr 1fr; + gap: clamp(24px, 5vw, 80px); + max-width: 1200px; margin-left: auto; - margin-right: auto; + margin-right: 0; + margin-top: 48px; + padding-left: clamp(20px, 14vw, 240px); + animation: rise 1.1s .8s both ease-out; } +.page--home .hero__lede { + font-size: clamp(15px, 1.1vw, 18px); + line-height: 1.6; + color: var(--paper-dim); + max-width: 48ch; +} +.page--home .hero__lede strong { color: var(--paper); font-weight: 600; } +.page--home .hero__lede a { color: var(--signal); border-bottom: 1px solid var(--rule); } +.page--home .hero__lede a:hover { color: var(--signal-2); border-color: var(--signal-2); } +.page--home .hero__lede code { + font-family: var(--mono); + font-size: 0.9em; + color: var(--signal-2); + background: rgba(255,90,31,0.08); + border-radius: 2px; + padding: 1px 5px; +} +.page--home .hero__lede--muted { color: var(--mute); } -/* ===== CODE BLOCKS ===== */ -[data-md-color-scheme="default"] .md-typeset pre > code { - background-color: #1a202c; +.page--home .dropcap { + font-family: var(--serif); + font-style: italic; + font-size: 3.4em; + line-height: 0.85; + float: left; + padding: 6px 10px 0 0; + color: var(--signal); + font-weight: 400; + font-variation-settings: "opsz" 144; } -[data-md-color-scheme="default"] .md-typeset code { - background-color: #edf2f7; +@media (max-width: 760px) { + .page--home .hero__stamp { + position: static; text-align: left; + border-right: 0; + border-left: 1px solid var(--signal); + padding-right: 0; padding-left: 14px; + margin-bottom: 36px; + } + .page--home .hero__intro { grid-template-columns: 1fr; padding-left: 0; } + .page--home .hero__row--2, .page--home .hero__row--3 { padding-left: 0; } + .page--home .hero__chip { display: none; } } -[data-md-color-scheme="slate"] .md-typeset pre > code { - background-color: #171923; +/* Ticker */ +.page--home .hero__ticker { + margin-top: 80px; + border-top: 1px solid var(--rule); + border-bottom: 1px solid var(--rule); + padding: 14px 0; + overflow: hidden; + position: relative; + animation: rise 1.1s 1s both ease-out; } +.page--home .ticker { overflow: hidden; } +.page--home .ticker__track { + display: flex; gap: 30px; + white-space: nowrap; + animation: ticker 60s linear infinite; + font-family: var(--mono); + font-size: 13px; + letter-spacing: 0.04em; + color: var(--mute); + text-transform: lowercase; +} +.page--home .ticker__track span { color: var(--paper-dim); } +.page--home .ticker__track span:nth-child(even) { color: var(--signal); } +@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } } -[data-md-color-scheme="slate"] .md-typeset code { - background-color: #2d3748; +@keyframes rise { + from { opacity: 0; transform: translateY(28px); } + to { opacity: 1; transform: translateY(0); } } -/* ===== LINKS ===== */ -.md-typeset a { - color: var(--tx-green); +/* ────────────────────────────────────────────────────────────── + GENERIC SECTION + ────────────────────────────────────────────────────────────── */ + +.page--home .section { + position: relative; + padding: clamp(64px, 11vh, 130px) var(--gutter); + max-width: 1500px; + margin: 0 auto; +} + +.page--home .section__head { + max-width: 1100px; + margin-bottom: 64px; +} +.page--home .section__index { + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--signal); + margin-bottom: 18px; + padding-bottom: 14px; + border-bottom: 1px solid var(--rule); +} +.page--home .section__title { + font-family: var(--serif); + font-weight: 300; + font-size: clamp(36px, 5.5vw, 76px); + line-height: 1.0; + letter-spacing: -0.025em; + color: var(--paper); + font-variation-settings: "opsz" 144; + margin-bottom: 24px; +} +.page--home .section__title em.serif { color: var(--signal); font-style: italic; } +.page--home .section__lede { + color: var(--paper-dim); + max-width: 64ch; + font-size: 17px; + line-height: 1.6; +} +.page--home .section__lede code { + font-family: var(--mono); + font-size: 0.9em; + color: var(--signal-2); + background: rgba(255,90,31,0.08); + border-radius: 2px; + padding: 1px 5px; +} + +/* ────────────────────────────────────────────────────────────── + FLAGSHIP CARDS + ────────────────────────────────────────────────────────────── */ + +.page--home .flagship { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} +@media (max-width: 980px) { + .page--home .flagship { grid-template-columns: 1fr; } +} + +.page--home .flagship__card { + position: relative; + background: var(--ink-2); + border: 1px solid var(--rule); + padding: 36px 36px 28px; + overflow: hidden; + transition: border-color .35s ease, transform .35s ease; +} +.page--home .flagship__card::before { + content: ""; + position: absolute; + top: 0; left: 0; + width: 56px; height: 1px; + background: var(--signal); + transition: width .5s cubic-bezier(.2,.8,.2,1); +} +.page--home .flagship__card:hover { border-color: var(--rule-2); transform: translateY(-3px); } +.page--home .flagship__card:hover::before { width: 100%; } + +.page--home .flagship__head { margin-bottom: 18px; } +.page--home .flagship__id { + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--mute); + margin-bottom: 14px; +} +.page--home .flagship__url { + color: var(--signal); + border-bottom: 1px solid transparent; + transition: border-color .2s, color .2s; + text-transform: lowercase; + letter-spacing: 0.06em; +} +.page--home .flagship__url:hover { color: var(--signal-2); border-bottom-color: var(--signal-2); } +.page--home .flagship__name { + font-family: var(--serif); + font-weight: 300; + font-style: italic; + font-size: clamp(48px, 5vw, 80px); + line-height: 0.95; + letter-spacing: -0.03em; + color: var(--paper); + font-variation-settings: "opsz" 144; + margin-bottom: 12px; +} +.page--home .flagship__tag { + font-family: var(--sans); + color: var(--paper-dim); + font-size: 18px; + line-height: 1.4; + max-width: 40ch; +} +.page--home .flagship__tag code { + font-size: .92em; + color: var(--signal); + background: rgba(255,90,31,0.08); + padding: 1px 6px; + border-radius: 2px; +} +.page--home .flagship__body p { + color: var(--paper-dim); + line-height: 1.6; + margin: 18px 0 26px; + max-width: 52ch; + font-size: 15.5px; +} +.page--home .flagship__body code { + color: var(--signal-2); + background: rgba(255,90,31,0.07); + padding: 1px 5px; + border-radius: 2px; + font-size: .9em; + font-family: var(--mono); +} +.page--home .flagship__body em.serif { color: var(--paper); } + +.page--home .flagship__demo { margin: 8px 0 28px; } + +.page--home .flagship__foot { + display: flex; flex-wrap: wrap; gap: 12px; align-items: center; + padding-top: 22px; + border-top: 1px dashed var(--rule); +} + +.page--home .flagship__card--cli { background: var(--ink-2); } +.page--home .flagship__card--lib { + background: + linear-gradient(135deg, rgba(255,90,31,0.025), transparent 60%), + var(--ink-2); +} + +/* Terminal (homepage demo block; .terminal is a common name so scope it) */ +.page--home .terminal { + background: #0A0A07; + border: 1px solid var(--rule); + border-radius: 4px; + overflow: hidden; + font-family: var(--mono); + box-shadow: 0 12px 36px rgba(0,0,0,0.4), inset 0 0 0 1px rgba(255,255,255,0.02); +} +.page--home .terminal__bar { + display: flex; align-items: center; gap: 8px; + padding: 9px 14px; + background: linear-gradient(180deg, #161410, #0E0D0A); + border-bottom: 1px solid var(--rule); + position: relative; +} +.page--home .terminal__bar span { + width: 10px; height: 10px; border-radius: 50%; + background: #2A2A22; +} +.page--home .terminal__bar span:nth-child(1) { background: #FF5F56; } +.page--home .terminal__bar span:nth-child(2) { background: #FFBD2E; } +.page--home .terminal__bar span:nth-child(3) { background: #27C93F; } +.page--home .terminal__bar em { + position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); + font-style: normal; + font-size: 11px; + color: var(--mute); + letter-spacing: 0.04em; + font-family: var(--mono); +} +.page--home .terminal__body { + padding: 18px 18px; + font-size: 12.5px; + line-height: 1.7; + color: var(--paper-dim); + white-space: pre; + overflow-x: auto; + margin: 0; + background: transparent; +} +.page--home .t-prompt { color: var(--signal); margin-right: 8px; } +.page--home .t-ok { color: var(--acid); } +.page--home .t-mute { color: var(--mute); } + +/* ────────────────────────────────────────────────────────────── + STACK LIST (features list, hover-shifts) + ────────────────────────────────────────────────────────────── */ + +.page--home .stack { + list-style: none; + border-top: 1px solid var(--rule); + margin: 0; + padding: 0; +} +.page--home .stack__row { + display: grid; + grid-template-columns: 80px 1fr 130px 40px; + align-items: center; + gap: 24px; + padding: 24px 8px; + border-bottom: 1px solid var(--rule); + transition: background .25s, padding .25s; + cursor: default; +} +.page--home .stack__row:hover { + background: var(--ink-2); + padding-left: 22px; + padding-right: 22px; +} +.page--home .stack__row:hover .stack__name { color: var(--signal); } +.page--home .stack__row:hover .stack__year { color: var(--signal); transform: translateX(4px); } +.page--home .stack__num { + font-family: var(--mono); + color: var(--mute-2); + font-size: 13px; + letter-spacing: 0.08em; +} +.page--home .stack__name { + display: block; + font-family: var(--serif); + font-style: italic; + font-size: clamp(24px, 2.5vw, 34px); + color: var(--paper); + line-height: 1.05; + transition: color .25s; + font-variation-settings: "opsz" 144; + margin-bottom: 6px; text-decoration: none; } +.page--home .stack__desc { + color: var(--paper-dim); + font-size: 14.5px; + max-width: 70ch; + line-height: 1.5; +} +.page--home .stack__desc code { + font-family: var(--mono); + font-size: 0.9em; + color: var(--signal-2); + background: rgba(255,90,31,0.08); + border-radius: 2px; + padding: 1px 5px; +} +.page--home .stack__tag { + font-family: var(--mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--mute); + text-align: right; + border: 1px solid var(--rule); + padding: 5px 10px; + border-radius: 2px; + background: rgba(0,0,0,0.2); + justify-self: end; +} +.page--home .stack__year { + font-family: var(--mono); + color: var(--mute); + text-align: right; + transition: transform .25s, color .25s; + font-size: 18px; +} +.page--home .stack__row--more .stack__name { + font-style: normal; + font-family: var(--sans); + font-size: 18px; + color: var(--paper-dim); +} -.md-typeset a:hover { - color: var(--tx-green-dark); - text-decoration: underline; +@media (max-width: 760px) { + .page--home .stack__row { grid-template-columns: 50px 1fr 50px; } + .page--home .stack__tag { display: none; } } -/* Navigation active */ -.md-nav__link--active { - color: var(--tx-green) !important; +/* ────────────────────────────────────────────────────────────── + CODA + ────────────────────────────────────────────────────────────── */ + +.page--home .section--coda { padding-top: 80px; padding-bottom: 140px; } +.page--home .coda { + max-width: 1000px; + margin: 0 auto; + text-align: left; + border-left: 1px solid var(--signal); + padding-left: clamp(28px, 5vw, 60px); +} +.page--home .coda__kicker { + font-family: var(--mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--mute); + margin-bottom: 24px; +} +.page--home .coda__big { + font-family: var(--serif); + font-weight: 300; + font-size: clamp(28px, 3.6vw, 52px); + line-height: 1.15; + letter-spacing: -0.02em; + color: var(--paper); + font-variation-settings: "opsz" 144; + margin-bottom: 48px; +} +.page--home .coda__big a { color: inherit; } +.page--home .coda__big em.serif { color: var(--signal); } +.page--home .coda__cta { + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + padding-top: 28px; + border-top: 1px dashed var(--rule); } -/* ===== TABLE HEADERS ===== */ -.md-typeset table:not([class]) th { - background-color: var(--tx-green); - color: white; +/* ────────────────────────────────────────────────────────────── + BUTTONS + ────────────────────────────────────────────────────────────── */ + +.page--home a.btn, +a.btn { + display: inline-flex; align-items: center; gap: 8px; + font-family: var(--mono); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--paper); + padding: 12px 20px; + border: 1px solid var(--rule-2); + border-radius: 2px; + background: transparent; + text-decoration: none; + transition: all .25s ease; + cursor: pointer; + line-height: 1; + font-weight: 700; +} +.page--home a.btn:hover, +a.btn:hover { + color: var(--ink); + background: var(--paper); + border-color: var(--paper); +} +.page--home a.btn--accent, +a.btn--accent { + background: var(--signal); + color: var(--ink); + border-color: var(--signal); +} +.page--home a.btn--accent:hover, +a.btn--accent:hover { background: var(--paper); color: var(--ink); border-color: var(--paper); } +.page--home a.btn--big, +a.btn--big { + font-size: 13px; + padding: 18px 28px; + background: var(--paper); + color: var(--ink); + border-color: var(--paper); + font-weight: 700; +} +.page--home a.btn--big:hover, +a.btn--big:hover { background: var(--signal); color: var(--ink); border-color: var(--signal); } + +a.btn:focus-visible { outline-offset: 4px; } + +/* ────────────────────────────────────────────────────────────── + HOMEPAGE FOOTER + Renamed from .footer to .home-footer to avoid collision with + any markdown that uses class="footer". The dead adjacent-sibling + selector .page--home + .footer never matched because Material + wraps both in .md-container. + ────────────────────────────────────────────────────────────── */ + +.home-footer { + position: relative; + z-index: 1; + background: var(--ink); + border-top: 1px solid var(--rule); + padding: 60px var(--gutter) 28px; + margin-top: 60px; +} +.home-footer__rule { + height: 1px; + background: linear-gradient(90deg, transparent, var(--rule-2), transparent); + margin-bottom: 50px; +} +.home-footer__inner { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr 1.1fr; + gap: 32px; + margin-bottom: 60px; +} +@media (max-width: 1180px) { + .home-footer__inner { grid-template-columns: 1fr 1fr 1fr; } +} +@media (max-width: 720px) { + .home-footer__inner { grid-template-columns: 1fr 1fr; } +} +@media (max-width: 480px) { + .home-footer__inner { grid-template-columns: 1fr; gap: 28px; } +} +.home-footer__col { min-width: 0; } +.home-footer__label { + font-family: var(--mono); + font-size: 11px; + color: var(--signal); + text-transform: uppercase; + letter-spacing: 0.18em; + margin: 0 0 14px; + padding-bottom: 10px; + border-bottom: 1px solid var(--rule); +} +.home-footer__text { + color: var(--paper-dim); + font-size: 14px; + line-height: 1.55; + max-width: 50ch; + margin: 0; +} +.home-footer__text a { + color: var(--paper); + border-bottom: 1px solid var(--rule-2); + text-decoration: none; + transition: color .2s, border-color .2s; +} +.home-footer__text a:hover { color: var(--signal); border-color: var(--signal); } +.home-footer__links { + list-style: none; + font-family: var(--mono); + font-size: 13px; + margin: 0; + padding: 0; +} +.home-footer__links li { padding: 4px 0; } +.home-footer__links a { + color: var(--paper-dim); + text-decoration: none; + transition: color .2s; +} +.home-footer__links a:hover { color: var(--signal); } +.home-footer__links__sub { + color: var(--mute); + font-style: italic; + font-family: var(--serif); + font-size: 12.5px; + margin-left: 4px; +} +.home-footer__col--meta .home-footer__mono { + font-family: var(--mono); + font-size: 12px; + color: var(--mute); + line-height: 1.7; + margin: 0; +} +.home-footer__col--meta .dot { color: var(--rule-2); margin: 0 4px; } + +.home-footer__base { + display: flex; justify-content: space-between; align-items: center; + flex-wrap: wrap; gap: 14px; + padding-top: 24px; + border-top: 1px solid var(--rule); + font-family: var(--mono); + font-size: 11px; + color: var(--mute); + letter-spacing: 0.04em; +} +.home-footer__base p { margin: 0; } +.home-footer__base__tag { color: var(--signal); } + +/* ────────────────────────────────────────────────────────────── + INNER PAGES / Material chrome restyle + These rules apply to all non-homepage pages. They re-skin + Material's components against the txn2 tokens. + ────────────────────────────────────────────────────────────── */ + +/* Header (kept on inner pages, rail-styled) */ +.md-header { + background: rgba(11,11,9,0.86); + backdrop-filter: blur(14px) saturate(140%); + -webkit-backdrop-filter: blur(14px) saturate(140%); + border-bottom: 1px solid var(--rule); + box-shadow: none !important; + color: var(--paper); +} +.md-header[data-md-state="shadow"] { box-shadow: none; } +.md-header__title { + font-family: var(--serif); + font-style: italic; + font-weight: 300; + font-size: 22px; + letter-spacing: -0.01em; + color: var(--paper); +} +.md-header__title .md-header__topic { + font-family: var(--serif); + font-style: italic; + font-weight: 300; + color: var(--paper); + transition: color .2s ease; +} +.md-header__button.md-logo img, +.md-header__button.md-logo svg { + filter: brightness(1.05); + transition: transform .25s ease, opacity .25s ease; +} +/* Hover affordance on the brand area. Material wraps the logo + title block + in an anchor to `/`, so making it visually clearly a link is enough. */ +a.md-header__button.md-logo:hover img, +a.md-header__button.md-logo:hover svg { transform: rotate(8deg); opacity: 0.85; } +a.md-header__button.md-logo:hover ~ .md-header__title .md-header__topic, +.md-header__title a:hover .md-header__topic { color: var(--signal); } + +/* Make the active tab visually loud so users can find Home from any page. */ +.md-tabs__link[href="."], +.md-tabs__link[href="./"] { + color: var(--signal); + font-weight: 700; +} +.md-tabs__link[href="."]::before, +.md-tabs__link[href="./"]::before { + content: "◐ "; + color: var(--signal); + margin-right: 4px; +} + +/* Tabs */ +.md-tabs { + background: var(--ink); + border-bottom: 1px solid var(--rule); + color: var(--paper-dim); +} +.md-tabs__link { + font-family: var(--mono); + font-size: 12px; + letter-spacing: 0.04em; + text-transform: lowercase; + opacity: 1; + color: var(--paper-dim); + margin-top: 0.6rem; + margin-bottom: 0.6rem; +} +.md-tabs__link:hover { color: var(--signal); } +.md-tabs__link--active { color: var(--signal); font-weight: 700; } + +/* Search */ +.md-search__form { + background: var(--ink-3); + border: 1px solid var(--rule); + border-radius: 2px; +} +.md-search__form:hover { background: var(--ink-3); border-color: var(--rule-2); } +[data-md-toggle="search"]:checked ~ .md-header .md-search__form { + background: var(--ink-3); + border-color: var(--signal); } +.md-search__input { + font-family: var(--mono); + color: var(--paper); +} +.md-search__input::placeholder { color: var(--mute); } +.md-search__icon { color: var(--mute); } +.md-search-result__meta { + background: var(--ink-2); + color: var(--mute); + font-family: var(--mono); + text-transform: lowercase; + letter-spacing: 0.08em; +} +.md-search-result__article { background: var(--ink); } +.md-search-result__teaser { color: var(--paper-dim); } +.md-search-result__link:hover { background: var(--ink-2); } + +/* Sidebar nav */ +.md-nav__title { + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--mute); + font-weight: 700; + background: transparent; + box-shadow: none; +} +.md-nav__link { + color: var(--paper-dim); + font-size: 14px; + font-family: var(--sans); +} +.md-nav__link:hover { color: var(--signal); } +.md-nav__link--active, +.md-nav__link--active code { color: var(--signal) !important; font-weight: 700; } + +.md-nav--secondary .md-nav__title { + background: transparent; + box-shadow: none; + color: var(--mute); +} + +/* Inner-page typography (Fraunces headings, paper body) */ +.md-content { background: transparent; color: var(--paper); } +.md-typeset { color: var(--paper); font-family: var(--sans); } + +.md-typeset h1 { + font-family: var(--serif); + font-weight: 300; + font-style: italic; + font-variation-settings: "opsz" 144; + font-size: clamp(36px, 4.4vw, 60px); + line-height: 1.0; + letter-spacing: -0.025em; + color: var(--paper); + margin: 0 0 0.6em; +} +.md-typeset h2 { + font-family: var(--serif); + font-weight: 300; + font-style: italic; + font-variation-settings: "opsz" 144; + font-size: clamp(26px, 2.8vw, 38px); + line-height: 1.05; + letter-spacing: -0.02em; + color: var(--paper); + margin: 1.6em 0 0.5em; +} +/* h3 and h4 are usually subsections / technical labels. + Use sans-serif so endpoint names and reference text stay readable. */ +.md-typeset h3 { + font-family: var(--sans); + font-style: normal; + font-weight: 700; + font-size: clamp(18px, 1.6vw, 22px); + line-height: 1.3; + letter-spacing: -0.01em; + color: var(--paper); + margin: 1.6em 0 0.4em; + padding-top: 0.4em; + border-top: 1px solid var(--rule); +} +.md-typeset h3:first-of-type { border-top: 0; padding-top: 0; } + +.md-typeset h4 { + font-family: var(--sans); + font-size: 16px; + font-weight: 700; + color: var(--paper); + letter-spacing: -0.005em; + margin: 1.4em 0 0.3em; +} +.md-typeset h5 { + font-family: var(--mono); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--paper); +} +.md-typeset h6 { + font-family: var(--mono); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--mute); +} + +/* Any heading containing inline code switches to mono entirely so the + technical reference reads as code, not as italic display text. + :has() is supported in Firefox 121+ (Dec 2023) and Safari 15.4+. */ +.md-typeset h1:has(code), +.md-typeset h2:has(code), +.md-typeset h3:has(code), +.md-typeset h4:has(code) { + font-family: var(--mono); + font-style: normal; + font-weight: 700; + letter-spacing: 0; + font-size: clamp(16px, 1.4vw, 20px); +} +.md-typeset h2:has(code) { font-size: clamp(20px, 1.8vw, 26px); } +.md-typeset h1:has(code) { font-size: clamp(24px, 2.2vw, 32px); } +.md-typeset h1:has(code) code, +.md-typeset h2:has(code) code, +.md-typeset h3:has(code) code, +.md-typeset h4:has(code) code { + background: transparent; + border: none; + padding: 0; + color: var(--signal); + font-size: 1em; +} + +/* Fallback for browsers without :has() support: code inside any heading + gets the mono+signal treatment even if the heading itself stays in + Fraunces. Less ideal but still readable. */ +@supports not selector(:has(*)) { + .md-typeset h1 code, + .md-typeset h2 code, + .md-typeset h3 code, + .md-typeset h4 code { + background: transparent; + border: 0; + padding: 0; + color: var(--signal); + font-family: var(--mono); + font-weight: 700; + font-style: normal; + font-size: 0.92em; + } +} + +/* Mermaid: container + a couple of element-level overrides for things + Material's variables don't fully cover. */ +.md-typeset .mermaid { + background: var(--ink-3); + border: 1px dashed var(--rule); + border-radius: 2px; + padding: 18px 20px; + margin: 1.4em 0; + font-family: var(--mono); + text-align: center; + overflow-x: auto; +} +.md-typeset .mermaid svg { max-width: 100%; height: auto; display: inline-block; } +.md-typeset .mermaid .edgeLabel { + background: var(--ink-3) !important; + color: var(--paper-dim) !important; + padding: 2px 6px !important; +} +.md-typeset .mermaid .cluster rect { fill: var(--ink-2) !important; stroke: var(--rule) !important; } + +/* Heading anchor link */ +.md-typeset .headerlink { + color: var(--mute-2); + border: none; +} +.md-typeset h1:hover .headerlink, +.md-typeset h2:hover .headerlink, +.md-typeset h3:hover .headerlink, +.md-typeset h4:hover .headerlink, +.md-typeset h5:hover .headerlink, +.md-typeset h6:hover .headerlink { color: var(--signal); } + +.md-typeset p, .md-typeset li { color: var(--paper); } +.md-typeset strong { color: var(--paper); font-weight: 700; } +.md-typeset em { font-family: var(--serif); font-style: italic; } + +.md-typeset a { + color: var(--signal); + border-bottom: 1px solid rgba(255,90,31,0.25); + text-decoration: none; + transition: color .15s, border-color .15s; +} +.md-typeset a:hover { color: var(--signal-2); border-bottom-color: var(--signal-2); } +.md-typeset a.md-button { border-bottom: none; } -/* ===== BUTTONS ===== */ -.md-typeset .md-button--primary { - background: var(--tx-green); - border-color: var(--tx-green); +/* Inline code */ +.md-typeset code { + background: var(--ink-2); + color: var(--signal-2); + border: 1px solid var(--rule); + border-radius: 2px; + padding: 0.1em 0.4em; + font-size: 0.88em; + font-family: var(--mono); +} + +/* Code blocks: single source of border + background, tight padding */ +.md-typeset .highlight, +.md-typeset .highlighttable { + background: var(--ink-2); + border: 1px solid var(--rule); + border-radius: 2px; + margin: 0.6em 0 1.2em; + position: relative; +} +.md-typeset pre { + background: transparent; + border: 0; + margin: 0; + border-radius: 0; +} +.md-typeset .highlight pre, +.md-typeset .highlighttable pre { + background: transparent; + border: 0; + margin: 0; + border-radius: 0; } +.md-typeset pre > code, +.md-typeset .highlight pre > code, +.md-typeset .highlighttable pre > code { + background: transparent; + border: 0; + padding: 14px 16px; + display: block; + color: var(--paper-dim); + font-size: 13px; + line-height: 1.55; + font-family: var(--mono); +} +.md-typeset .highlight .filename { + background: var(--ink-3); + color: var(--mute); + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 8px 14px; + border-bottom: 1px solid var(--rule); +} +.md-clipboard { + color: var(--mute); + background: rgba(11,11,9,0.6); + border-radius: 2px; +} +.md-clipboard:hover { color: var(--signal); } -.md-typeset .md-button--primary:hover { - background: var(--tx-green-dark); - border-color: var(--tx-green-dark); +/* Tighten paragraph→code spacing so labels read with their commands */ +.md-typeset p + .highlight, +.md-typeset p + .highlighttable, +.md-typeset p + pre { + margin-top: 0.4em; +} +.md-typeset .highlight + p, +.md-typeset .highlighttable + p, +.md-typeset pre + p { + margin-top: 1.4em; } -/* ===== SIDEBAR & NAV (DARK MODE) ===== */ -[data-md-color-scheme="slate"] .md-sidebar { - background-color: #1a202c; +/* Tighten paragraph spacing inside the typeset prose */ +.md-typeset p { margin: 0 0 1em; line-height: 1.6; } + +/* Admonitions */ +.md-typeset .admonition, +.md-typeset details { + background: var(--ink-2); + border: 1px solid var(--rule); + border-left: 2px solid var(--signal); + border-radius: 2px; + box-shadow: none; + font-size: 14.5px; + color: var(--paper-dim); +} +.md-typeset .admonition-title, +.md-typeset summary { + background: transparent; + font-family: var(--mono); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--paper); + border-bottom: 1px solid var(--rule); } +.md-typeset .admonition-title::before, +.md-typeset summary::before { background-color: var(--signal); } + +.md-typeset .admonition.note, +.md-typeset details.note { border-left-color: var(--cool); } +.md-typeset .admonition.warning, +.md-typeset details.warning { border-left-color: var(--signal); } +.md-typeset .admonition.danger, +.md-typeset details.danger { border-left-color: var(--signal); } +.md-typeset .admonition.tip, +.md-typeset details.tip { border-left-color: var(--acid); } +.md-typeset .admonition.success, +.md-typeset details.success { border-left-color: var(--acid); } -[data-md-color-scheme="slate"] .md-nav { - background-color: #1a202c; +/* Tables */ +.md-typeset table:not([class]) { + background: transparent; + border: 1px solid var(--rule); + border-radius: 0; + font-size: 14px; } +.md-typeset table:not([class]) th { + background: var(--ink-2); + color: var(--paper); + font-family: var(--mono); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + border-bottom: 1px solid var(--rule-2); +} +.md-typeset table:not([class]) td { + border-top: 1px solid var(--rule); + color: var(--paper-dim); +} +.md-typeset table:not([class]) tr:hover { background: var(--ink-2); } -/* ===== FOOTER ===== */ -[data-md-color-scheme="default"] .md-footer-meta { - background-color: #f7fafc; +/* Tabbed content: no outer box, just a label row underline. The code blocks + inside each tab are the visual content; a wrapping box makes nested + rectangles. */ +.md-typeset .tabbed-set { + background: transparent; + border: 0; + border-radius: 0; + margin: 1.2em 0 1.6em; + padding: 0; +} +.md-typeset .tabbed-labels { + border-bottom: 1px solid var(--rule); + background: transparent; + margin-bottom: 18px; +} +.md-typeset .tabbed-labels > label { + font-family: var(--mono); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--mute); + padding: 12px 18px; + margin: 0; + border-bottom: 2px solid transparent; + transition: color .2s ease, border-color .2s ease; +} +.md-typeset .tabbed-labels > label:hover { color: var(--paper); } +.md-typeset .tabbed-set > input:checked + label { + color: var(--signal); + border-color: var(--signal); +} +.md-typeset .tabbed-content { + padding: 0; } +.md-typeset .tabbed-block { + padding: 0; +} +.md-typeset .tabbed-block > p:first-child { margin-top: 0; } +.md-typeset .tabbed-block > p { + color: var(--paper-dim); + font-size: 15px; + margin: 1.1em 0 0.4em; +} +.md-typeset .tabbed-block > p:first-of-type { margin-top: 0; } -[data-md-color-scheme="slate"] .md-footer-meta { - background-color: #1a202c; +/* Scrollbar */ +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: var(--ink-2); } +::-webkit-scrollbar-thumb { background: var(--rule-2); border: 2px solid var(--ink-2); border-radius: 0; } +::-webkit-scrollbar-thumb:hover { background: var(--mute-2); } + +/* Material footer (inner pages) */ +.md-footer { + background: var(--ink-2); + border-top: 1px solid var(--rule); +} +.md-footer-meta { + background: var(--ink); + border-top: 1px solid var(--rule); } +.md-footer-meta__inner { + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.04em; + text-transform: lowercase; +} +.md-copyright, +.md-copyright a { color: var(--mute); font-size: 11px; } +.md-copyright a:hover { color: var(--signal); } +.md-social a { color: var(--mute); } +.md-social a:hover { color: var(--signal); } +.md-footer__title { background: var(--ink); color: var(--paper); } +.md-footer__direction { color: var(--mute); font-family: var(--mono); } +.md-footer__link { color: var(--paper-dim); } +.md-footer__link:hover { color: var(--signal); opacity: 1; } + +/* Hide Material's default content button on homepage */ +.page--home .md-content__button { display: none; } + +/* ────────────────────────────────────────────────────────────── + REDUCED MOTION + ────────────────────────────────────────────────────────────── */ -/* ===== HIDE LEFT SIDEBAR (flat nav uses tabs only) ===== */ -.md-sidebar--primary { - display: none; +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .001s !important; + animation-iteration-count: 1 !important; + transition-duration: .001s !important; + } + .ticker__track { animation: none; } + .rail__mark { animation: none; } } diff --git a/mkdocs.yml b/mkdocs.yml index be1db1d..9ff167c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,19 +14,8 @@ theme: logo: images/txn2-logo.png favicon: images/logo.png palette: - - media: "(prefers-color-scheme: light)" - scheme: default - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - toggle: - icon: material/brightness-4 - name: Switch to light mode - font: - text: Inter - code: JetBrains Mono + scheme: slate + font: false icon: repo: fontawesome/brands/github features: @@ -35,9 +24,10 @@ theme: - navigation.tracking - navigation.tabs - navigation.tabs.sticky + - navigation.sections + - navigation.expand - navigation.top - navigation.footer - - toc.integrate - search.suggest - search.highlight - search.share From ec2b905fa7245c023d1b48b4b1519f9d6d2aa3ce Mon Sep 17 00:00:00 2001 From: cjimti Date: Sun, 26 Apr 2026 17:41:17 -0700 Subject: [PATCH 2/2] fix(docs): address review findings on design system PR - home.html: stack list "in-memory mode" linked to a non-existent anchor (library/#in-memory-mode). The actual heading slug on library.md is from-a-string-in-memory. Fixed. - home.html, mkdocs.yml: copyright start year and est. stamp said 2018, but the first commit on master is 2019-02-15. Corrected the rail stamp to "est. 2019" and the copyright in both home footer and mkdocs.yml to 2019-2026. - home.html: rail and hero version stamp said "v1.x". Pinned to the actual current release v1.8.0 (verified via gh api). - main.html: skip-to-content link only existed on the homepage template. Added a header-block override so every inner page gets one too, with a focusable #main anchor injected at the top of the content area to give the skiplink a target. Material's
element has no id by default. --- docs/overrides/home.html | 10 +++++----- docs/overrides/main.html | 16 ++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 95793a4..ff8366a 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -21,7 +21,7 @@ /etc/hosts management for go
- v1.x + v1.8.0 ·· UTC @@ -62,7 +62,7 @@

project / txeh

org / txn2

-

est. 2018

+

est. 2019

pkg.go.dev ↗

apache 2.0 · cross platform

@@ -70,7 +70,7 @@

txeh - v1.x · go + v1.8.0 · go /etc/hosts @@ -263,7 +263,7 @@

  • 006
    - in-memory mode for tests + in-memory mode for tests

    Pass RawText to NewHosts and operate on a string. Save() errors as expected (no path); RenderHostsFile() returns the mutated text. Useful in unit tests and ephemeral fixtures.

    testing @@ -346,7 +346,7 @@

  • diff --git a/docs/overrides/main.html b/docs/overrides/main.html index c0cab4d..f4fc516 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -18,3 +18,19 @@ {% endblock %} + +{# Inner-page skip-to-content. The homepage template overrides block header + wholesale and includes its own skiplink targeting #main. #} +{% block header %} + +{{ super() }} +{% endblock %} + +{# Inner-page skip target. Material's
    has no + id, so we drop a focusable anchor at the top of the content area. The + homepage template overrides block container wholesale and sets id="main" + on its own
    element. #} +{% block content %} + +{{ super() }} +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index 9ff167c..9d1f853 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,7 @@ repo_url: https://github.com/txn2/txeh repo_name: txn2/txeh edit_uri: edit/master/docs/ -copyright: Copyright © 2018 - 2026 Craig Johnston / Deasil Works, Inc. +copyright: Copyright © 2019 - 2026 Craig Johnston / Deasil Works, Inc. theme: name: material