diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f6578f --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Build artifacts +arcline-landing +landing + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + diff --git a/Dockerfile b/Dockerfile index 76f9a6b..9ae03e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o landing . # ── Stage 2: Runtime ──────────────────────────────────────────────────────── FROM alpine:3.21 AS runtime -RUN apk add --no-cache ca-certificates && \ +RUN apk add --no-cache ca-certificates wget && \ addgroup -S arcline && \ adduser -S -G arcline arcline diff --git a/docker-compose.yml b/docker-compose.yml index f02edd2..e76d8ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,3 @@ -# Arcline Project — Landing Page -# -# Deploy: docker compose up -d -# Stop: docker compose down -# Logs: docker compose logs -f -# Build: docker compose build - services: landing: build: @@ -17,7 +10,7 @@ services: environment: - PORT=8080 healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080"] + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/healthz"] interval: 30s timeout: 3s retries: 3 diff --git a/main.go b/main.go index 69c7103..33e6528 100644 --- a/main.go +++ b/main.go @@ -1,32 +1,158 @@ package main import ( + "context" "html/template" "log" "net/http" "os" + "os/signal" + "strings" + "syscall" + "time" ) +var templates = make(map[string]*template.Template) + +// contextKey is used to pass per-request log extras from handlers to the logging middleware. +type contextKey struct{} +type logContext struct{ extra string } + func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } + // Parse templates once at startup. + index, err := template.ParseFiles("templates/base.html", "templates/index.html") + if err != nil { + log.Fatalf("failed to parse index template: %v", err) + } + templates["index"] = index + + waitlist, err := template.ParseFiles("templates/partials/waitlist_confirmation.html") + if err != nil { + log.Fatalf("failed to parse waitlist template: %v", err) + } + templates["waitlist"] = waitlist + + mux := http.NewServeMux() + // Serve static assets fs := http.FileServer(http.Dir("static")) - http.Handle("/static/", http.StripPrefix("/static/", fs)) + mux.Handle("/static/", http.StripPrefix("/static/", fs)) // Page routes - http.HandleFunc("/", handleIndex) + mux.HandleFunc("/", handleIndex) + mux.HandleFunc("/healthz", handleHealthz) // HTMX partial routes - http.HandleFunc("/partials/waitlist", handleWaitlist) + mux.HandleFunc("/partials/waitlist", handleWaitlist) + + // Wrap with logging (outermost) and security headers (innermost). + handler := loggingMiddleware(securityHeaders(mux)) + + srv := &http.Server{ + Addr: ":" + port, + Handler: handler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + // Graceful shutdown on SIGINT / SIGTERM. + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + log.Println("shutting down…") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Printf("shutdown error: %v", err) + } + }() log.Printf("Arcline Project server starting on :%s", port) - if err := http.ListenAndServe(":"+port, nil); err != nil { + if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("server failed: %v", err) } + log.Println("server stopped") +} + +// securityHeaders adds basic security headers to every response. +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") + next.ServeHTTP(w, r) + }) +} + +// responseWriter wraps http.ResponseWriter to capture the status code and bytes written. +type responseWriter struct { + http.ResponseWriter + status int + bytes int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.status = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + if rw.status == 0 { + rw.status = http.StatusOK + } + n, err := rw.ResponseWriter.Write(b) + rw.bytes += n + return n, err +} + +// loggingMiddleware logs every request with IP, method, path, status, size, duration, referer, and user-agent. +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + lc := &logContext{} + ctx := context.WithValue(r.Context(), contextKey{}, lc) + r = r.WithContext(ctx) + + start := time.Now() + rw := &responseWriter{ResponseWriter: w, status: 0} + next.ServeHTTP(rw, r) + + ip := r.RemoteAddr + if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { + ip = fwd + } + + ref := r.Referer() + if ref == "" { + ref = "-" + } + + ua := r.UserAgent() + if ua == "" { + ua = "-" + } + + duration := time.Since(start) + + if lc.extra != "" { + log.Printf("%s %s %s → %d (%dB) %v ref:%q ua:%q | %s", ip, r.Method, r.URL.Path, rw.status, rw.bytes, duration, ref, ua, lc.extra) + } else { + log.Printf("%s %s %s → %d (%dB) %v ref:%q ua:%q", ip, r.Method, r.URL.Path, rw.status, rw.bytes, duration, ref, ua) + } + }) +} + +func handleHealthz(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) } func handleIndex(w http.ResponseWriter, r *http.Request) { @@ -35,20 +161,13 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { return } - tmpl, err := template.ParseFiles("templates/base.html", "templates/index.html") - if err != nil { - log.Printf("template error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - + w.Header().Set("Content-Type", "text/html; charset=utf-8") data := map[string]interface{}{ "Title": "Arcline Project", } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := tmpl.ExecuteTemplate(w, "base", data); err != nil { + if err := templates["index"].ExecuteTemplate(w, "base", data); err != nil { log.Printf("render error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } @@ -58,23 +177,25 @@ func handleWaitlist(w http.ResponseWriter, r *http.Request) { return } - email := r.FormValue("email") - - // Parse edition preferences (checkboxes with same name) - r.ParseForm() - editions := r.Form["edition"] - log.Printf("Waitlist signup: %s | editions: %v", email, editions) - - tmpl, err := template.ParseFiles("templates/partials/waitlist_confirmation.html") - if err != nil { - log.Printf("template error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + if err := r.ParseForm(); err != nil { + http.Error(w, "Bad Request", http.StatusBadRequest) return } + email := strings.TrimSpace(r.FormValue("email")) + if email == "" || !strings.Contains(email, "@") { + http.Error(w, "Invalid email address", http.StatusBadRequest) + return + } + + // Signal the logging middleware that this was a waitlist signup. + if lc, ok := r.Context().Value(contextKey{}).(*logContext); ok { + lc.extra = "signup" + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := tmpl.Execute(w, map[string]string{"Email": email}); err != nil { + if err := templates["waitlist"].Execute(w, map[string]string{"Email": email}); err != nil { log.Printf("render error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } - diff --git a/static/css/style.css b/static/css/style.css index e2c380c..fa3082a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -20,7 +20,7 @@ --amber: #f09d51; --amber-bg: hsla(28 84% 63% / 0.07); --mono: "JetBrains Mono", "Fira Code", "SF Mono", "Cascadia Code", monospace; - --sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --pad-card: 22px; --w-main: 1040px; --w-narrow: 680px; @@ -137,7 +137,7 @@ body::after { z-index: 1; } -.hero-badge { +.hero-kicker { display: inline-block; font-family: var(--mono); font-size: 0.72rem; @@ -148,1719 +148,11 @@ body::after { margin-bottom: 32px; } -.hero-badge::before { - content: "$ "; - color: var(--text-faint); -} - -.hero h1 { - font-size: clamp(2.4rem, 6vw, 4rem); - font-weight: 750; - letter-spacing: -0.035em; - line-height: 1.08; - margin-bottom: 26px; - color: var(--text); -} - -.hero-highlight { color: var(--accent); } - -.tagline { - font-size: 1.05rem; - color: var(--text-soft); - margin-bottom: 42px; - line-height: 1.7; - max-width: 520px; - margin-left: auto; - margin-right: auto; -} - -.hero-actions { - display: flex; - gap: 14px; - justify-content: center; - flex-wrap: wrap; -} - -.cta { - display: inline-flex; - align-items: center; - background: var(--accent); - color: hsl(0 0% 8%); - padding: 13px 30px; - font-size: 0.92rem; - font-weight: 620; - text-decoration: none; - transition: background 0.18s, transform 0.14s; - font-family: var(--mono); - letter-spacing: -0.01em; -} - -.cta:hover { - background: var(--accent-dim); - transform: translateY(-1px); -} - -.cta-secondary { - display: inline-flex; - align-items: center; - color: var(--text-soft); - padding: 13px 30px; - font-size: 0.92rem; - font-weight: 470; - text-decoration: none; - border: 1px solid var(--border); - transition: border-color 0.18s, color 0.18s; -} - -.cta-secondary:hover { - border-color: var(--border-hover); - color: var(--text); -} - -/* Sections */ -.section { - padding: 105px 28px; - position: relative; -} - -.section-alt { background: var(--bg-raised); } - -.section-inner { - max-width: var(--w-main); - margin: 0 auto; -} - -.section-label { - display: block; - font-family: var(--mono); - font-size: 0.7rem; - color: var(--accent); - margin-bottom: 14px; - text-transform: lowercase; - letter-spacing: 0.04em; -} - -.section-label::before { content: "// "; color: var(--text-faint); } - -.section-inner h2 { - font-size: clamp(1.6rem, 3.6vw, 2.35rem); - font-weight: 680; - margin-bottom: 18px; - letter-spacing: -0.02em; - line-height: 1.22; - color: var(--text); - max-width: 680px; -} - -.section-desc { - color: var(--text-soft); - font-size: 1rem; - max-width: 590px; - margin-bottom: 52px; - line-height: 1.7; -} - -/* Principles */ -.principles-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(302px, 1fr)); - gap: 10px; -} - -.principle { - display: flex; - gap: 17px; - background: var(--bg-card); - border: 1px solid var(--border); - padding: var(--pad-card); - transition: border-color 0.22s; - position: relative; - overflow: hidden; -} - -.principle::after { - content: ""; - position: absolute; - inset: -1px; - background: var(--accent-bg); - opacity: 0; - transition: opacity 0.25s; - pointer-events: none; -} - -.principle:hover { border-color: var(--accent-border); } -.principle:hover::after { opacity: 1; } - -.principle-icon { - flex-shrink: 0; - width: 38px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - color: var(--accent); - margin-top: 2px; -} - -.principle h3 { - font-family: var(--mono); - font-size: 0.83rem; - font-weight: 620; - margin-bottom: 4px; - color: var(--text); - letter-spacing: -0.01em; -} - -.principle p { - color: var(--text-soft); - font-size: 0.845rem; - line-height: 1.6; -} - -/* Stack Grid */ -.stack-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 10px; -} - -.stack-card { - background: var(--bg-card); - border: 1px solid var(--border); - overflow: hidden; - transition: border-color 0.22s; - position: relative; -} - -.stack-card::before { - content: ""; - position: absolute; - top: 0; left: 0; bottom: 0; - width: 3px; - transition: width 0.2s; -} - -.stack-card:nth-child(1)::before { background: var(--accent); } -.stack-card:nth-child(2)::before { background: hsl(280 45% 55%); } -.stack-card:nth-child(3)::before { background: hsl(200 60% 52%); } -.stack-card:nth-child(4)::before { background: var(--amber); } - -.stack-card:hover { border-color: var(--border-hover); } -.stack-card:hover::before { width: 5px; } - -.stack-header { - font-family: var(--mono); - font-size: 0.7rem; - font-weight: 650; - text-transform: lowercase; - letter-spacing: 0.05em; - padding: 20px 22px 10px 22px; - color: var(--text-faint); -} - -.stack-card:nth-child(1) .stack-header { color: var(--accent); } -.stack-card:nth-child(2) .stack-header { color: hsl(280 45% 65%); } -.stack-card:nth-child(3) .stack-header { color: hsl(200 60% 58%); } -.stack-card:nth-child(4) .stack-header { color: var(--amber); } - -.stack-card ul { - list-style: none; - padding: 0 22px 22px 22px; - margin: 0; -} - -.stack-card li { - position: relative; - padding: 6px 0 6px 16px; - color: var(--text-soft); - font-size: 0.83rem; - line-height: 1.5; -} - -.stack-card li::before { - content: ""; - position: absolute; - left: 0; - top: 12px; - width: 5px; - height: 5px; -} - -.stack-card:nth-child(1) li::before { background: var(--accent); opacity: 0.55; } -.stack-card:nth-child(2) li::before { background: hsl(280 45% 55%); opacity: 0.55; } -.stack-card:nth-child(3) li::before { background: hsl(200 60% 52%); opacity: 0.55; } -.stack-card:nth-child(4) li::before { background: var(--amber); opacity: 0.55; } - -/* Edition Teaser */ -.edition-teaser { - margin-top: 30px; - padding: 20px 24px; - border: 1px dashed var(--accent-border); - background: hsla(162 88% 43% / 0.04); - position: relative; -} - -.edition-teaser-badge { - display: inline-block; - font-family: var(--mono); - font-size: 0.64rem; - color: var(--accent-dim); - margin-bottom: 8px; -} - -.edition-teaser p { - color: var(--text-soft); - font-size: 0.88rem; - line-height: 1.7; - max-width: 700px; -} - -.edition-teaser strong { color: var(--text); font-weight: 660; } - -/* Tool Grid */ -.tool-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(296px, 1fr)); - gap: 8px; -} - -.tool { - display: flex; - flex-direction: column; - gap: 5px; - background: var(--bg-card); - border: 0 solid var(--accent-border); - border-left-width: 2px; - padding: 19px 22px; - transition: border-color 0.2s, background 0.18s, border-left-width 0.2s; -} - -.tool:hover { - border-left-width: 4px; - border-left-color: var(--accent); - background: var(--bg-card-hover); -} - -.tool code { - font-family: var(--mono); - font-size: 0.82rem; - font-weight: 580; - color: var(--accent); - letter-spacing: -0.01em; -} - -.tool span { - color: var(--text-soft); - font-size: 0.81rem; - line-height: 1.55; -} - -/* Compare Grid */ -.compare-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 10px; -} - -.compare-card { - background: var(--bg-card); - border: 1px solid var(--border); - padding: 26px 22px; - transition: border-color 0.22s, background 0.2s; -} - -.compare-card:hover { border-color: var(--border-hover); } - -.compare-highlight { - border-color: var(--accent-border); - background: linear-gradient(155deg, var(--bg-card) 0%, hsla(162 88% 43% / 0.06) 100%); -} - -.compare-label { - font-family: var(--mono); - font-size: 0.72rem; - font-weight: 620; - text-transform: lowercase; - letter-spacing: 0.02em; - margin-bottom: 10px; - color: var(--text-faint); -} - -.compare-highlight .compare-label { color: var(--accent); } - -.compare-desc { - color: var(--text-soft); - font-size: 0.88rem; - line-height: 1.6; -} - -/* Contribute Grid */ -.contribute-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); - gap: 10px; -} - -.contribute-card { - display: flex; - flex-direction: column; - gap: 10px; - background: var(--bg-card); - border: 1px solid var(--border); - padding: 28px 22px; - color: var(--text); - text-decoration: none; - transition: border-color 0.22s, background 0.2s; - position: relative; -} - -.contribute-card::after { - content: " →"; - position: absolute; - right: 22px; - top: 28px; - font-family: var(--mono); - color: var(--accent); - opacity: 0; - transform: translateX(-8px); - transition: opacity 0.2s, transform 0.2s; -} - -.contribute-card:hover { border-color: var(--accent-border); } -.contribute-card:hover::after { opacity: 1; transform: translateX(0); } - -.contribute-card span { - font-size: 0.96rem; - font-weight: 640; - letter-spacing: -0.01em; -} - -.contribute-card small { - color: var(--text-soft); - font-size: 0.82rem; - line-height: 1.5; -} - -.contribute-icon { - width: 34px; - height: 34px; - display: flex; - align-items: center; - justify-content: center; - background: hsla(162 88% 43% / 0.07); - margin-bottom: 2px; -} - -/* Inline Link */ -.inline-link { - color: var(--accent); - text-decoration: none; - border-bottom: 1px solid var(--accent-border); - padding-bottom: 1px; - transition: border-color 0.18s; -} - -.inline-link:hover { border-bottom-color: var(--accent); } - -/* Sponsor */ -.sponsor-section .section-inner { text-align: left; } - -.sponsor-content { - display: flex; - flex-direction: column; - gap: 22px; - max-width: 620px; -} - -.sponsor-desc { - max-width: 600px; - color: var(--text-soft); - font-size: 0.94rem; - line-height: 1.7; -} - -.sponsor-desc strong { color: var(--text); font-weight: 660; } - -.sponsor-tagline { - font-family: var(--mono); - font-size: 0.78rem; - color: var(--text-faint); - padding: 8px 0; -} - -.sponsor-tagline::before { content: "# "; color: var(--border-hover); } - -/* Waitlist */ -.waitlist-form { - display: flex; - flex-direction: column; - gap: 18px; - margin-top: 6px; - max-width: 480px; -} - -.waitlist-form .input, -.waitlist-form .btn { - width: 100%; -} - -/* Edition Checkboxes */ -.edition-checkboxes { - border: none; - padding: 0; - display: flex; - gap: 22px; -} - -.edition-checkboxes legend { - font-size: 0.83rem; - color: var(--text-soft); - margin-bottom: 10px; - padding: 0; - float: left; - width: 100%; -} - -.edition-checkbox { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 0.88rem; - color: var(--text-soft); - user-select: none; - transition: color 0.18s; -} - -.edition-checkbox:hover { color: var(--text); } - -.edition-checkbox input[type="checkbox"] { - appearance: none; - -webkit-appearance: none; - width: 18px; - height: 18px; - border: 2px solid var(--border); - background: var(--bg-card); - cursor: pointer; - position: relative; - flex-shrink: 0; - transition: border-color 0.18s, background 0.18s; -} - -.edition-checkbox input[type="checkbox"]:checked { - border-color: var(--accent); - background: var(--accent); -} - -.edition-checkbox input[type="checkbox"]:checked::after { - content: ""; - position: absolute; - left: 4px; - top: 2px; - width: 5px; - height: 9px; - border: solid hsl(0 0% 8%); - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -/* Waitlist Form Row */ -.waitlist-row { - display: flex; - gap: 11px; -} - -.input { - padding: 13px 18px; - border: 1px solid var(--border); - border-radius: 3px; - background: var(--bg-card); - color: var(--text); - font-size: 0.9rem; - min-width: 310px; - outline: none; - transition: border-color 0.18s, box-shadow 0.18s; - font-family: var(--mono); -} - -.input::placeholder { color: var(--text-faint); font-family: var(--mono); } - -.input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-bg); -} - -.btn { - padding: 13px 30px; - border: none; - border-radius: 3px; - background: var(--accent); - color: hsl(0 0% 8%); - font-size: 0.9rem; - font-weight: 620; - cursor: pointer; - transition: background 0.18s; - font-family: var(--mono); - letter-spacing: -0.01em; -} - -.btn:hover { background: var(--accent-dim); } - -.confirmation { - margin-top: 18px; - padding: 12px 20px; - color: var(--accent); - background: var(--accent-bg); - border: 1px solid var(--accent-border); - font-weight: 520; - font-size: 0.88rem; - display: inline-block; - font-family: var(--mono); -} - -.waitlist-note { - margin-top: 22px; - font-size: 0.8rem; - color: var(--text-faint); -} - -/* Footer */ -.footer { - padding: 44px 28px 30px; - border-top: 1px solid var(--border); -} - -.footer-inner { - max-width: var(--w-main); - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: flex-start; - flex-wrap: wrap; - gap: 22px; -} - -.footer-brand p { - color: var(--text-soft); - font-size: 0.84rem; - margin-top: 5px; -} - -.footer-logo { - font-family: var(--mono); - font-size: 1rem; - font-weight: 620; - color: var(--text); - text-decoration: none; -} - -.footer-links { display: flex; gap: 22px; } - -.footer-links a { - color: var(--text-soft); - text-decoration: none; - font-size: 0.84rem; - transition: color 0.18s; -} - -.footer-links a:hover { color: var(--text); } - -.footer-bottom { - max-width: var(--w-main); - margin: 28px auto 0; - padding-top: 18px; - border-top: 1px solid var(--border); - text-align: left; -} - -.footer-bottom p { - font-family: var(--mono); - color: var(--text-faint); - font-size: 0.74rem; -} - -/* Responsive */ -@media (max-width: 920px) { - .stack-grid { grid-template-columns: repeat(2, 1fr); } -} - -@media (max-width: 768px) { - .hero { padding: 90px 22px 72px; } - .section { padding: 68px 22px; } - .hero-actions { flex-direction: column; align-items: stretch; } - .cta, .cta-secondary { justify-content: center; text-align: center; } - .principles-grid { grid-template-columns: 1fr; } - .tool-grid { grid-template-columns: 1fr; } -} - -@media (max-width: 580px) { - .hero { padding: 62px 18px 50px; } - .hero h1 { font-size: 1.85rem; } - .section { padding: 50px 18px; } - .section-inner h2 { font-size: 1.4rem; } - .nav nav a { padding: 7px 9px; font-size: 0.76rem; } - .input { min-width: 100%; } - .btn { width: 100%; } - .stack-grid { grid-template-columns: 1fr; } - .compare-grid { grid-template-columns: 1fr; } - .contribute-grid { grid-template-columns: 1fr; } - .footer-inner { flex-direction: column; } -} - - -======= -/* ======================================== - Arcline Project — Landing Page - Terminal-native. Hand-crafted. - No rounded corners. No box-shadows. - No AI slop. - ======================================== */ - -*, -*::before, -*::after { box-sizing: border-box; margin: 0; padding: 0; } - -:root { - --bg: #0b0c0e; - --bg-alt: #0f1115; - --bg-card: #13151a; - --border: #1e2028; - --border-dim: #181a20; - --text: #d4d6dc; - --text-dim: #8b8f9a; - --text-faint: #555a66; - --accent: #0acf97; - --accent-dim: #078a65; - --accent-bg: rgba(10, 207, 151, 0.06); - --amber: #e8954a; - --amber-dim: #a5682e; - --red: #e0556a; - --mono: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", "IBM Plex Mono", monospace; - --w-main: 1060px; - --w-narrow: 640px; -} - -html { - scroll-behavior: smooth; - font-size: 16px; - -webkit-text-size-adjust: 100%; -} - -body { - font-family: var(--mono); - background: var(--bg); - color: var(--text); - line-height: 1.7; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - position: relative; -} - -/* Subtle noise overlay — kept from original, it's good */ -body::after { - content: ""; - position: fixed; - inset: 0; - z-index: 9999; - pointer-events: none; - opacity: 0.018; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); -} - -/* ============================================ - TYPOGRAPHY - ============================================ */ - -h1, h2, h3, h4 { - font-family: var(--mono); - font-weight: 600; - line-height: 1.25; - color: var(--text); -} - -p, li, a, span, small, label, legend, input, button { - font-family: var(--mono); -} - -a { - color: var(--accent); - text-decoration: none; - transition: color 0.15s; -} - -a:hover { color: var(--accent-dim); } - -::selection { - background: var(--accent); - color: var(--bg); -} - -/* ============================================ - NAVIGATION - ============================================ */ - -.nav { - position: sticky; - top: 0; - z-index: 100; - background: rgba(11, 12, 14, 0.92); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-bottom: 1px solid var(--border-dim); -} - -.nav-inner { - max-width: var(--w-main); - margin: 0 auto; - padding: 0 32px; - height: 56px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.logo { - display: flex; - align-items: center; - gap: 10px; - font-size: 0.92rem; - font-weight: 600; - color: var(--text); - text-decoration: none; - letter-spacing: -0.01em; -} - -.logo-icon { flex-shrink: 0; } - -.nav nav { display: flex; gap: 0; } - -.nav nav a { - color: var(--text-dim); - text-decoration: none; - padding: 6px 14px; - font-size: 0.78rem; - font-weight: 500; - transition: color 0.15s; - border: 1px solid transparent; -} - -.nav nav a:hover { - color: var(--accent); - background: transparent; -} - -/* ============================================ - HERO - ============================================ */ - -.hero { - padding: 140px 32px 120px; - position: relative; - overflow: hidden; - text-align: left; -} - -.hero::before { - content: ""; - position: absolute; - top: -30%; - left: 20%; - width: 800px; - height: 800px; - background: radial-gradient(ellipse at center, rgba(10, 207, 151, 0.06) 0%, transparent 60%); - pointer-events: none; -} - -.hero::after { - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - opacity: 0.025; - background: - linear-gradient(0deg, transparent 48%, var(--border-dim) 48%, var(--border-dim) 52%, transparent 52%); - background-size: 80px 80px; -} - -.hero-inner { - max-width: var(--w-main); - margin: 0 auto; - position: relative; - z-index: 1; -} - -.hero-kicker { - font-size: 0.72rem; - color: var(--accent); - margin-bottom: 28px; - letter-spacing: 0.03em; -} - .hero-kicker::before { content: "$ "; color: var(--text-faint); } -.hero h1 { - font-size: clamp(2.2rem, 5.4vw, 3.6rem); - font-weight: 700; - letter-spacing: -0.03em; - line-height: 1.12; - margin-bottom: 28px; - color: var(--text); - max-width: 820px; -} - -.hero-highlight { - color: var(--accent); - position: relative; -} - -.tagline { - font-size: 0.92rem; - color: var(--text-dim); - margin-bottom: 48px; - line-height: 1.8; - max-width: 540px; - border-left: 2px solid var(--accent); - padding-left: 20px; -} - -.hero-actions { - display: flex; - gap: 16px; - flex-wrap: wrap; -} - -.cta { - display: inline-flex; - align-items: center; - background: var(--accent); - color: #0a0c0e; - padding: 14px 32px; - font-size: 0.88rem; - font-weight: 600; - text-decoration: none; - transition: background 0.15s; - letter-spacing: -0.01em; -} - -.cta:hover { - background: var(--accent-dim); - color: #0a0c0e; -} - -.cta-secondary { - display: inline-flex; - align-items: center; - color: var(--text-dim); - padding: 14px 32px; - font-size: 0.88rem; - font-weight: 500; - text-decoration: none; - border: 1px solid var(--border); - transition: border-color 0.15s, color 0.15s; -} - -.cta-secondary:hover { - border-color: var(--accent-dim); - color: var(--accent); -} - -/* ============================================ - SECTIONS - ============================================ */ - -.section { - padding: 100px 32px; - position: relative; -} - -.section + .section { - border-top: 1px solid var(--border-dim); -} - -.section-alt { - background: var(--bg-alt); - border-top: 1px solid var(--border-dim); - border-bottom: 1px solid var(--border-dim); -} - -.section-inner { - max-width: var(--w-main); - margin: 0 auto; -} - -.section-label { - display: block; - font-size: 0.68rem; - color: var(--accent); - margin-bottom: 16px; - letter-spacing: 0.05em; -} - -.section-label::before { - content: "// "; - color: var(--text-faint); -} - -.section-inner h2 { - font-size: clamp(1.5rem, 3.2vw, 2.1rem); - font-weight: 680; - margin-bottom: 20px; - letter-spacing: -0.025em; - line-height: 1.28; - color: var(--text); - max-width: 680px; -} - -.section-desc { - color: var(--text-dim); - font-size: 0.88rem; - max-width: 560px; - margin-bottom: 56px; - line-height: 1.8; -} - -/* ============================================ - PRINCIPLES (About section) - ============================================ */ - -.principles-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1px; - background: var(--border-dim); - border: 1px solid var(--border-dim); -} - -.principle { - display: flex; - gap: 18px; - background: var(--bg); - padding: 26px 24px; - transition: background 0.2s; - position: relative; -} - -.section-alt .principle { background: var(--bg-alt); } - -.principle::before { - content: ""; - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 0; - background: var(--accent); - transition: width 0.18s; -} - -.principle:hover::before { width: 3px; } -.principle:hover { background: var(--bg-card); } -.section-alt .principle:hover { background: var(--bg-card); } - -.principle-icon { - flex-shrink: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - color: var(--accent); - margin-top: 1px; -} - -.principle h3 { - font-size: 0.84rem; - font-weight: 620; - margin-bottom: 5px; - color: var(--text); -} - -.principle p { - color: var(--text-dim); - font-size: 0.8rem; - line-height: 1.7; -} - -/* ============================================ - STACK GRID - ============================================ */ - -.stack-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1px; - background: var(--border-dim); - border: 1px solid var(--border-dim); -} - -.stack-card { - background: var(--bg); - padding: 0; - position: relative; - overflow: hidden; -} - -.section-alt .stack-card { background: var(--bg-alt); } - -.stack-card::before { - content: ""; - position: absolute; - top: 0; left: 0; bottom: 0; - width: 2px; - transition: width 0.18s; -} - -.stack-card:nth-child(1)::before { background: var(--accent); } -.stack-card:nth-child(2)::before { background: #8b6eb8; } -.stack-card:nth-child(3)::before { background: #4da6d9; } -.stack-card:nth-child(4)::before { background: var(--amber); } - -.stack-card:hover::before { width: 4px; } - -.stack-header { - font-size: 0.66rem; - font-weight: 650; - letter-spacing: 0.04em; - padding: 22px 22px 8px 26px; -} - -.stack-card:nth-child(1) .stack-header { color: var(--accent); } -.stack-card:nth-child(2) .stack-header { color: #9b80c9; } -.stack-card:nth-child(3) .stack-header { color: #5bb8e8; } -.stack-card:nth-child(4) .stack-header { color: var(--amber); } - -.stack-card ul { - list-style: none; - padding: 0 22px 22px 26px; - margin: 0; -} - -.stack-card li { - position: relative; - padding: 5px 0 5px 14px; - color: var(--text-dim); - font-size: 0.78rem; - line-height: 1.6; -} - -.stack-card li::before { - content: ">"; - position: absolute; - left: 0; - top: 5px; - font-size: 0.6rem; - color: var(--text-faint); -} - -/* Edition teaser */ -.edition-teaser { - margin-top: 24px; - padding: 20px 24px; - border: 1px dashed var(--border); - position: relative; -} - -.edition-teaser-badge { - display: inline-block; - font-size: 0.64rem; - color: var(--text-faint); - margin-bottom: 10px; -} - -.edition-teaser p { - color: var(--text-dim); - font-size: 0.84rem; - line-height: 1.7; -} - -.edition-teaser strong { color: var(--text); font-weight: 650; } - -/* ============================================ - TOOL GRID - ============================================ */ - -.tool-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); - gap: 1px; - background: var(--border-dim); - border: 1px solid var(--border-dim); -} - -.tool { - display: flex; - flex-direction: column; - gap: 6px; - background: var(--bg-alt); - padding: 20px 24px; - transition: background 0.15s; - position: relative; -} - -.tool::before { - content: ""; - position: absolute; - top: 0; left: 0; bottom: 0; - width: 0; - background: var(--accent); - transition: width 0.15s; -} - -.tool:hover { background: var(--bg-card); } -.tool:hover::before { width: 3px; } - -.tool code { - font-size: 0.82rem; - font-weight: 580; - color: var(--accent); -} - -.tool span { - color: var(--text-dim); - font-size: 0.78rem; - line-height: 1.6; -} - -/* ============================================ - COMPARISON GRID - ============================================ */ - -.compare-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 1px; - background: var(--border-dim); - border: 1px solid var(--border-dim); -} - -.compare-card { - background: var(--bg); - padding: 28px 24px; - position: relative; - transition: background 0.15s; -} - -.compare-card:hover { background: var(--bg-card); } - -.compare-card::before { - content: ""; - position: absolute; - top: 0; left: 0; bottom: 0; - width: 0; - transition: width 0.15s; -} - -.compare-highlight::before { - background: var(--accent); - width: 3px; -} - -.compare-highlight { - background: var(--bg-card); -} - -.compare-label { - font-size: 0.7rem; - font-weight: 620; - letter-spacing: 0.03em; - margin-bottom: 10px; - color: var(--text-faint); -} - -.compare-highlight .compare-label { color: var(--accent); } - -.compare-desc { - color: var(--text-dim); - font-size: 0.82rem; - line-height: 1.7; -} - -/* ============================================ - CONTRIBUTE GRID - ============================================ */ - -.contribute-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 1px; - background: var(--border-dim); - border: 1px solid var(--border-dim); -} - -.contribute-card { - display: flex; - flex-direction: column; - gap: 12px; - background: var(--bg-alt); - padding: 30px 26px; - color: var(--text); - text-decoration: none; - transition: background 0.15s; - position: relative; -} - -.contribute-card:hover { - background: var(--bg-card); - color: var(--text); -} - -.contribute-card::after { - content: "->"; - position: absolute; - right: 26px; - top: 30px; - color: var(--text-faint); - opacity: 0; - transform: translateX(-6px); - transition: opacity 0.2s, transform 0.2s; -} - -.contribute-card:hover::after { - opacity: 1; - transform: translateX(0); - color: var(--accent); -} - -.contribute-card span { - font-size: 0.9rem; - font-weight: 630; -} - -.contribute-card small { - color: var(--text-dim); - font-size: 0.78rem; - line-height: 1.6; -} - -.contribute-icon { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - color: var(--accent); - margin-bottom: 2px; -} - -/* ============================================ - INLINE LINK - ============================================ */ - -.inline-link { - color: var(--accent); - text-decoration: none; - border-bottom: 1px solid rgba(10, 207, 151, 0.25); - padding-bottom: 1px; - transition: border-color 0.15s; -} - -.inline-link:hover { border-bottom-color: var(--accent); } - -/* ============================================ - SPONSOR - ============================================ */ - -.sponsor-content { - display: flex; - flex-direction: column; - gap: 24px; - max-width: 600px; -} - -.sponsor-desc { - color: var(--text-dim); - font-size: 0.88rem; - line-height: 1.8; -} - -.sponsor-desc strong { color: var(--text); font-weight: 650; } - -.sponsor-tagline { - font-size: 0.76rem; - color: var(--text-faint); -} - -.sponsor-tagline::before { - content: "# "; - color: var(--text-faint); -} - -/* ============================================ - WAITLIST / FORM - ============================================ */ - -.waitlist-form { - display: flex; - flex-direction: column; - gap: 18px; - margin-top: 6px; - max-width: 440px; -} - -.waitlist-form .input, -.waitlist-form .btn { - width: 100%; -} - -.edition-checkboxes { - border: none; - padding: 0; - display: flex; - gap: 26px; -} - -.edition-checkboxes legend { - font-size: 0.78rem; - color: var(--text-dim); - margin-bottom: 10px; - padding: 0; - float: left; - width: 100%; -} - -.edition-checkbox { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 0.84rem; - color: var(--text-dim); - user-select: none; - transition: color 0.15s; -} - -.edition-checkbox:hover { color: var(--text); } - -.edition-checkbox input[type="checkbox"] { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - border: 2px solid var(--border); - background: var(--bg-card); - cursor: pointer; - position: relative; - flex-shrink: 0; - transition: border-color 0.15s, background 0.15s; -} - -.edition-checkbox input[type="checkbox"]:checked { - border-color: var(--accent); - background: var(--accent); -} - -.edition-checkbox input[type="checkbox"]:checked::after { - content: ""; - position: absolute; - left: 3px; - top: 1px; - width: 5px; - height: 9px; - border: solid var(--bg); - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -.input { - padding: 13px 18px; - border: 1px solid var(--border); - background: var(--bg-card); - color: var(--text); - font-size: 0.86rem; - outline: none; - transition: border-color 0.15s; -} - -.input::placeholder { color: var(--text-faint); } - -.input:focus { - border-color: var(--accent); -} - -.btn { - padding: 14px 32px; - border: none; - background: var(--accent); - color: #0a0c0e; - font-size: 0.88rem; - font-weight: 620; - cursor: pointer; - transition: background 0.15s; - letter-spacing: -0.01em; -} - -.btn:hover { background: var(--accent-dim); } - -.confirmation { - margin-top: 18px; - padding: 14px 22px; - color: var(--accent); - background: var(--accent-bg); - border: 1px solid rgba(10, 207, 151, 0.18); - font-weight: 540; - font-size: 0.84rem; - display: inline-block; -} - -.waitlist-note { - margin-top: 22px; - font-size: 0.76rem; - color: var(--text-faint); - max-width: 400px; -} - -/* ============================================ - FOOTER - ============================================ */ - -.footer { - padding: 48px 32px 32px; - border-top: 1px solid var(--border-dim); -} - -.footer-inner { - max-width: var(--w-main); - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: flex-start; - flex-wrap: wrap; - gap: 22px; -} - -.footer-brand p { - color: var(--text-dim); - font-size: 0.8rem; - margin-top: 6px; -} - -.footer-logo { - font-size: 0.92rem; - font-weight: 620; - color: var(--text); - text-decoration: none; -} - -.footer-links { display: flex; gap: 24px; } - -.footer-links a { - color: var(--text-dim); - text-decoration: none; - font-size: 0.8rem; - transition: color 0.15s; -} - -.footer-links a:hover { color: var(--accent); } - -.footer-bottom { - max-width: var(--w-main); - margin: 28px auto 0; - padding-top: 20px; - border-top: 1px solid var(--border-dim); - text-align: left; -} - -.footer-bottom p { - color: var(--text-faint); - font-size: 0.7rem; -} - -/* ============================================ - RESPONSIVE - ============================================ */ - -@media (max-width: 920px) { - .stack-grid { grid-template-columns: repeat(2, 1fr); } -} - -@media (max-width: 768px) { - .hero { - padding: 100px 24px 80px; - text-align: left; - } - .hero h1 { font-size: 1.9rem; } - .tagline { padding-left: 16px; } - .section { padding: 68px 24px; } - .hero-actions { flex-direction: column; align-items: stretch; } - .cta, .cta-secondary { justify-content: center; text-align: center; } - .principles-grid { grid-template-columns: 1fr; } - .tool-grid { grid-template-columns: 1fr; } - .compare-grid { grid-template-columns: repeat(2, 1fr); } -} - -@media (max-width: 580px) { - .hero { padding: 72px 18px 56px; } - .hero h1 { font-size: 1.55rem; } - .section { padding: 50px 18px; } - .section-inner h2 { font-size: 1.3rem; } - .nav nav a { padding: 6px 8px; font-size: 0.72rem; } - .input { min-width: 100%; } - .btn { width: 100%; } - .stack-grid { grid-template-columns: 1fr; } - .compare-grid { grid-template-columns: 1fr; } - .contribute-grid { grid-template-columns: 1fr; } - .footer-inner { flex-direction: column; } - .nav-inner { padding: 0 18px; } -} - ======================================== */ - -*, -*::before, -*::after { box-sizing: border-box; margin: 0; padding: 0; } - -:root { - --bg: #0d0e10; - --bg-raised: #15171b; - --bg-card: #1a1c22; - --bg-card-hover: #1f2128; - --border: hsl(225 7% 18%); - --border-hover: hsl(225 8% 26%); - --text: hsl(223 8% 88%); - --text-soft: hsl(223 6% 65%); - --text-faint: hsl(223 5% 41%); - --accent: #0acf97; - --accent-dim: hsl(162 88% 33%); - --accent-bg: hsla(162 88% 43% / 0.09); - --accent-border: hsla(162 88% 43% / 0.18); - --amber: #f09d51; - --amber-bg: hsla(28 84% 63% / 0.07); - --mono: "JetBrains Mono", "Fira Code", "SF Mono", "Cascadia Code", monospace; - --sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --pad-card: 22px; - --w-main: 1040px; - --w-narrow: 680px; -} - -html { scroll-behavior: smooth; } - -body { - font-family: var(--sans); - background: var(--bg); - color: var(--text); - line-height: 1.65; - -webkit-font-smoothing: antialiased; -} - -body::after { - content: ""; - position: fixed; - inset: 0; - z-index: 9999; - pointer-events: none; - opacity: 0.022; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); -} - -/* Nav */ -.nav { - position: sticky; - top: 0; - z-index: 100; - background: hsl(225 6% 6% / 0.88); - backdrop-filter: blur(18px); - -webkit-backdrop-filter: blur(18px); - border-bottom: 1px solid var(--border); -} - -.nav-inner { - max-width: var(--w-main); - margin: 0 auto; - padding: 0 28px; - height: 58px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.logo { - display: flex; - align-items: center; - gap: 11px; - font-family: var(--mono); - font-size: 1.05rem; - font-weight: 600; - color: var(--text); - text-decoration: none; - letter-spacing: -0.01em; -} - -.logo-icon { flex-shrink: 0; } - -.nav nav { display: flex; gap: 2px; } - -.nav nav a { - color: var(--text-soft); - text-decoration: none; - padding: 7px 14px; - border-radius: 5px; - font-size: 0.825rem; - font-weight: 470; - transition: color 0.18s, background 0.18s; -} - -.nav nav a:hover { - color: var(--text); - background: hsl(225 6% 14%); -} - -/* Hero */ -.hero { - padding: 130px 28px 110px; - text-align: center; - position: relative; - overflow: hidden; -} - -.hero::before { - content: ""; - position: absolute; - top: -40%; - left: 30%; - width: 960px; - height: 960px; - background: radial-gradient(ellipse at center, hsla(162 88% 43% / 0.075) 0%, transparent 60%); - pointer-events: none; -} - -.hero::after { - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - opacity: 0.03; - background: - linear-gradient(25deg, transparent 48%, var(--border) 48%, var(--border) 52%, transparent 52%), - linear-gradient(25deg, transparent 48%, var(--border) 48%, var(--border) 52%, transparent 52%); - background-size: 90px 90px; - background-position: 0 0, 45px 45px; -} - -.hero-inner { - max-width: var(--w-narrow); - margin: 0 auto; - position: relative; - z-index: 1; -} - -.hero-badge { - display: inline-block; - font-family: var(--mono); - font-size: 0.72rem; - color: var(--accent); - background: hsla(162 88% 43% / 0.06); - border: 1px solid var(--accent-border); - padding: 5px 14px; - margin-bottom: 32px; -} - -.hero-badge::before { - content: "$ "; - color: var(--text-faint); -} - .hero h1 { font-size: clamp(2.4rem, 6vw, 4rem); font-weight: 750; @@ -1929,6 +221,7 @@ body::after { .section { padding: 105px 28px; position: relative; + text-align: center; } .section-alt { background: var(--bg-raised); } @@ -1954,6 +247,8 @@ body::after { font-size: clamp(1.6rem, 3.6vw, 2.35rem); font-weight: 680; margin-bottom: 18px; + margin-left: auto; + margin-right: auto; letter-spacing: -0.02em; line-height: 1.22; color: var(--text); @@ -1965,6 +260,8 @@ body::after { font-size: 1rem; max-width: 590px; margin-bottom: 52px; + margin-left: auto; + margin-right: auto; line-height: 1.7; } @@ -2121,6 +418,8 @@ body::after { font-size: 0.88rem; line-height: 1.7; max-width: 700px; + margin-left: auto; + margin-right: auto; } .edition-teaser strong { color: var(--text); font-weight: 660; } @@ -2205,7 +504,7 @@ body::after { /* Contribute Grid */ .contribute-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 10px; } @@ -2256,6 +555,7 @@ body::after { align-items: center; justify-content: center; background: hsla(162 88% 43% / 0.07); + color: var(--accent); margin-bottom: 2px; } @@ -2305,6 +605,8 @@ body::after { gap: 18px; margin-top: 6px; max-width: 480px; + margin-left: auto; + margin-right: auto; } .waitlist-form .input, @@ -2515,3 +817,4 @@ body::after { .footer-inner { flex-direction: column; } } + diff --git a/templates/base.html b/templates/base.html index 326ea34..acfc256 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,6 +6,9 @@ {{.Title}} + + + diff --git a/templates/index.html b/templates/index.html index 1fefe95..b2b79f6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,7 +22,6 @@
-
arcline os — alpha

The Linux OS for
people who run infrastructure. @@ -181,7 +180,7 @@

-
// also in the works
+
// arcline workstation — in development

Arcline Workstation — same hardened base, with a lightweight KDE Plasma desktop, pre-configured dev toolchains, and privacy-hardened browser profiles.