From d688053161519b9a020941557b306d45d1e41fcb Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Fri, 22 May 2026 01:15:48 -0400 Subject: [PATCH 1/2] Outpost-centered responsive main layout with status header (#70). Wide/medium/narrow breakpoints place the schematic center or full-width; day, scenario, and badges move to the top strip. --- internal/ui/header.go | 26 ++++++++++ internal/ui/layout.go | 82 ++++++++++++++++++++++-------- internal/ui/layout_test.go | 16 ++++++ internal/ui/resource_panel.go | 4 -- internal/ui/resource_panel_test.go | 10 ++-- internal/ui/update.go | 2 +- internal/ui/view.go | 18 +++++-- internal/ui/widgets.go | 2 +- test/internal/ui/layout_test.go | 56 ++++++++++++-------- 9 files changed, 159 insertions(+), 57 deletions(-) create mode 100644 internal/ui/header.go create mode 100644 internal/ui/layout_test.go diff --git a/internal/ui/header.go b/internal/ui/header.go new file mode 100644 index 0000000..7dda7ce --- /dev/null +++ b/internal/ui/header.go @@ -0,0 +1,26 @@ +package ui + +import ( + "fmt" + "strings" + + "github.com/ericdahl/outpost-404/internal/game" +) + +// RenderStatusHeader is the top title strip: game name, day, run profile, status badges. +func RenderStatusHeader(s game.State, profiles game.RunProfiles, _ int) string { + scName := s.ScenarioID + if sc, ok := profiles.FindScenario(s.ScenarioID); ok { + scName = sc.Name + } + diffName := s.DifficultyID + if d, ok := profiles.FindDifficulty(s.DifficultyID); ok { + diffName = d.Name + } + line := fmt.Sprintf("Outpost 404 — Day %d / %d — %s · %s", + s.Day, s.SurvivalWinTarget(), scName, diffName) + if strip := formatStatusStrip(game.StatusBadges(s)); strip != "" { + line += " — " + strings.TrimPrefix(strip, "Status: ") + } + return titleStyle.Render("Outpost 404") + "\n" + line +} diff --git a/internal/ui/layout.go b/internal/ui/layout.go index 075d67c..020b696 100644 --- a/internal/ui/layout.go +++ b/internal/ui/layout.go @@ -1,8 +1,19 @@ package ui +// LayoutMode selects the main-screen panel arrangement. +type LayoutMode int + +const ( + LayoutModeNarrow LayoutMode = iota + LayoutModeMedium + LayoutModeWide +) + // MainLayout describes how the main screen arranges panels for the current terminal size. type MainLayout struct { + Mode LayoutMode Stacked bool + OutpostBelow bool ContentWidth int LeftWidth int MiddleWidth int @@ -12,40 +23,71 @@ type MainLayout struct { } const ( - horizontalMinWidth = 96 - minPanelWidth = 24 - maxLeftWidth = 36 - maxMiddleWidth = 42 + wideMinWidth = 120 + wideMinHeight = 35 + mediumMinWidth = 100 + mediumMinHeight = 30 + minPanelWidth = 24 + maxLeftWidth = 36 + maxMiddleWidth = 42 ) func MainLayoutFor(termWidth, termHeight int) MainLayout { - _ = termHeight content := contentWidth(termWidth) - logW := LogViewportWidth(termWidth) + logW := LogViewportWidth(termWidth, termHeight) l := MainLayout{ ContentWidth: content, LogInnerWidth: logW, BoxWidth: boxWidth(termWidth, 80), CompactKeys: termWidth < 88, } - if termWidth < horizontalMinWidth { - l.Stacked = true - l.LeftWidth = content - l.MiddleWidth = content - return l + switch { + case termWidth >= wideMinWidth && termHeight >= wideMinHeight: + l.Mode = LayoutModeWide + l.applyWideLayout(termWidth, logW) + case termWidth >= mediumMinWidth && termHeight >= mediumMinHeight: + l.Mode = LayoutModeMedium + l.applyMediumLayout(termWidth, logW, content) + default: + l.Mode = LayoutModeNarrow + l.applyNarrowLayout(content) } + return l +} + +func (l *MainLayout) applyWideLayout(termWidth, logW int) { l.Stacked = false + l.OutpostBelow = false logBox := logW + 2 remaining := termWidth - logBox if remaining < minPanelWidth*2 { - l.Stacked = true - l.LeftWidth = content - l.MiddleWidth = content - return l + l.applyNarrowLayout(l.ContentWidth) + l.Mode = LayoutModeNarrow + return } l.LeftWidth = clamp(minPanelWidth, remaining*9/20, maxLeftWidth) l.MiddleWidth = clamp(minPanelWidth, remaining-l.LeftWidth, maxMiddleWidth) - return l +} + +func (l *MainLayout) applyMediumLayout(termWidth, logW, content int) { + l.Stacked = false + l.OutpostBelow = true + l.MiddleWidth = content + logBox := logW + 2 + remaining := termWidth - logBox + if remaining < minPanelWidth { + l.applyNarrowLayout(content) + l.Mode = LayoutModeNarrow + return + } + l.LeftWidth = clamp(minPanelWidth, remaining, maxLeftWidth) +} + +func (l *MainLayout) applyNarrowLayout(content int) { + l.Stacked = true + l.OutpostBelow = false + l.LeftWidth = content + l.MiddleWidth = content } func contentWidth(termWidth int) int { @@ -67,19 +109,19 @@ func boxWidth(termWidth, preferred int) int { return w } -func LogViewportWidth(termWidth int) int { - if termWidth < horizontalMinWidth { +func LogViewportWidth(termWidth, termHeight int) int { + if termWidth < mediumMinWidth || termHeight < mediumMinHeight { return clamp(22, termWidth-8, 44) } w := 44 - if termWidth > 120 { + if termWidth >= wideMinWidth { return 52 } return w } func LogViewportHeight(termHeight int) int { - h := termHeight - 12 + h := termHeight - 14 return clamp(6, h, 14) } diff --git a/internal/ui/layout_test.go b/internal/ui/layout_test.go new file mode 100644 index 0000000..accec7f --- /dev/null +++ b/internal/ui/layout_test.go @@ -0,0 +1,16 @@ +package ui + +import "testing" + +func TestMainLayout_wideUsesThreeColumns(t *testing.T) { + l := MainLayoutFor(120, 35) + if l.Mode != LayoutModeWide || l.OutpostBelow { + t.Fatalf("got mode=%v outpostBelow=%v", l.Mode, l.OutpostBelow) + } +} + +func TestMainLayout_logWidthOnWide(t *testing.T) { + if w := LogViewportWidth(120, 35); w < 44 { + t.Fatalf("log width=%d on wide terminal", w) + } +} diff --git a/internal/ui/resource_panel.go b/internal/ui/resource_panel.go index 0c628de..be44d43 100644 --- a/internal/ui/resource_panel.go +++ b/internal/ui/resource_panel.go @@ -16,16 +16,12 @@ func RenderResourcePanel(s game.State, panelWidth int) string { barW := resourceBarWidth(panelWidth) lines := []string{ - fmt.Sprintf("Day %d / %d", s.Day, s.SurvivalWinTarget()), resourceBarLine("Power", s.Power, deltas.Power, barW, game.WarningPowerCriticalAt, game.WarningPowerUrgentAt), resourceBarLine("Food", s.Food, deltas.Food, barW, game.WarningFoodCriticalAt, game.WarningFoodUrgentAt), resourceBarLine("Morale", s.Morale, deltas.Morale, barW, game.WarningMoraleCriticalAt, game.WarningMoraleUrgentAt), fmt.Sprintf("Credits %-4d Pop %d/%d Beacon %d/%d", s.Credits, s.Population, s.PopulationCap, s.BeaconParts, s.MaxBeaconParts), } - if strip := formatStatusStrip(game.StatusBadges(s)); strip != "" { - lines = append(lines, "", strip) - } return title + "\n" + strings.Join(lines, "\n") } diff --git a/internal/ui/resource_panel_test.go b/internal/ui/resource_panel_test.go index d617a78..7a09a0e 100644 --- a/internal/ui/resource_panel_test.go +++ b/internal/ui/resource_panel_test.go @@ -41,14 +41,10 @@ func TestFormatStatusStrip_JoinsBadges(t *testing.T) { } } -func TestRenderResourcePanel_IncludesStatusStrip(t *testing.T) { +func TestRenderResourcePanel_IncludesResourceBars(t *testing.T) { s := game.NewState(game.Content{Buildings: []game.BuildingDef{{ID: "habitat", Name: "Habitat", MaxLevel: 3}}}) - s.Food = game.WarningFoodCriticalAt body := RenderResourcePanel(s, 36) - if !strings.Contains(body, "Status:") { - t.Fatalf("missing status strip:\n%s", body) - } - if !strings.Contains(body, "Power") { - t.Fatalf("missing power bar:\n%s", body) + if !strings.Contains(body, "Power") || !strings.Contains(body, "Food") { + t.Fatalf("missing resource bars:\n%s", body) } } diff --git a/internal/ui/update.go b/internal/ui/update.go index 37e37ec..0f7f038 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -13,7 +13,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.TermWidth = msg.Width m.TermHeight = msg.Height - m.LogViewport.Width = LogViewportWidth(msg.Width) + m.LogViewport.Width = LogViewportWidth(msg.Width, msg.Height) m.LogViewport.Height = LogViewportHeight(msg.Height) m.LogViewport = syncLogViewport(m.LogViewport, m.State.Log) if m.Started { diff --git a/internal/ui/view.go b/internal/ui/view.go index c394fd2..7bc2f8b 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -41,14 +41,24 @@ func (m Model) mainView() string { "Event Log\n" + mutedStyle.Render("↑↓ scroll") + "\n" + m.LogViewport.View(), ) + header := RenderStatusHeader(m.State, m.Profiles, m.TermWidth) var panels string - if layout.Stacked { - panels = lipgloss.JoinVertical(lipgloss.Left, left, middle, logPanel) - } else { + switch layout.Mode { + case LayoutModeMedium: + top := lipgloss.JoinHorizontal(lipgloss.Top, left, logPanel) + panels = lipgloss.JoinVertical(lipgloss.Left, top, middle) + case LayoutModeWide: panels = lipgloss.JoinHorizontal(lipgloss.Top, left, middle, logPanel) + default: + parts := []string{left} + if strip := formatStatusStrip(game.StatusBadges(m.State)); strip != "" { + parts = append(parts, boxStyle.Width(layout.ContentWidth).Render(strip)) + } + parts = append(parts, middle, logPanel) + panels = lipgloss.JoinVertical(lipgloss.Left, parts...) } - return titleStyle.Render("Outpost 404") + "\n" + mutedStyle.Render("A tiny terminal base builder by default, a future colony sim by design.") + "\n\n" + panels + "\n\n" + mainActions(layout.CompactKeys) + return header + "\n\n" + panels + "\n\n" + mainActions(layout.CompactKeys) } func mainActions(compact bool) string { diff --git a/internal/ui/widgets.go b/internal/ui/widgets.go index 94f808c..aca2436 100644 --- a/internal/ui/widgets.go +++ b/internal/ui/widgets.go @@ -103,7 +103,7 @@ func buildListHeight(itemCount int) int { } func newLogViewport(termWidth, termHeight int) viewport.Model { - vp := viewport.New(LogViewportWidth(termWidth), LogViewportHeight(termHeight)) + vp := viewport.New(LogViewportWidth(termWidth, termHeight), LogViewportHeight(termHeight)) vp.Style = lipgloss.NewStyle() return vp } diff --git a/test/internal/ui/layout_test.go b/test/internal/ui/layout_test.go index 640e3a3..df2c69a 100644 --- a/test/internal/ui/layout_test.go +++ b/test/internal/ui/layout_test.go @@ -1,40 +1,56 @@ package ui_test import ( + "strings" "testing" + "github.com/ericdahl/outpost-404/internal/game" "github.com/ericdahl/outpost-404/internal/ui" ) -func TestMainLayout_wideTerminalUsesHorizontalPanels(t *testing.T) { - l := ui.MainLayoutFor(120, 30) - if l.Stacked { - t.Fatal("expected horizontal layout at 120 columns") +func TestMainLayout_wideAt120x35(t *testing.T) { + l := ui.MainLayoutFor(120, 35) + if l.Mode != ui.LayoutModeWide { + t.Fatalf("mode = %v, want wide", l.Mode) } - if l.LeftWidth < 28 || l.MiddleWidth < 28 { - t.Fatalf("panel widths too small: left=%d mid=%d", l.LeftWidth, l.MiddleWidth) + if l.Stacked || l.OutpostBelow { + t.Fatalf("wide should be 3-column row: stacked=%v outpostBelow=%v", l.Stacked, l.OutpostBelow) } - if l.LogInnerWidth < 44 { - t.Fatalf("log width=%d, want at least 44 on wide terminal", l.LogInnerWidth) +} + +func TestMainLayout_mediumAt100x30(t *testing.T) { + l := ui.MainLayoutFor(100, 30) + if l.Mode != ui.LayoutModeMedium { + t.Fatalf("mode = %v, want medium", l.Mode) + } + if !l.OutpostBelow || l.Stacked { + t.Fatalf("medium want outpost below: stacked=%v outpostBelow=%v", l.Stacked, l.OutpostBelow) } } -func TestMainLayout_narrowTerminalStacksPanels(t *testing.T) { - l := ui.MainLayoutFor(70, 24) - if !l.Stacked { - t.Fatal("expected stacked layout below horizontal breakpoint") +func TestMainLayout_narrowAt80x24(t *testing.T) { + l := ui.MainLayoutFor(80, 24) + if l.Mode != ui.LayoutModeNarrow { + t.Fatalf("mode = %v, want narrow", l.Mode) } - if l.ContentWidth < 60 { - t.Fatalf("content width=%d, want most of terminal width", l.ContentWidth) + if !l.Stacked { + t.Fatal("narrow should stack panels") } } -func TestMainLayout_logViewportShrinksOnNarrowTerminal(t *testing.T) { - w := ui.LogViewportWidth(60) - if w > 52 { - t.Fatalf("log viewport width=%d, should shrink with terminal", w) +func TestRenderStatusHeader_IncludesDayAndScenario(t *testing.T) { + s := game.NewState(game.Content{}) + s.Day = 18 + s.ScenarioID = "standard" + s.DifficultyID = "normal" + p := game.RunProfiles{ + Scenarios: []game.ScenarioDef{{ID: "standard", Name: "Standard Landing"}}, + Difficulties: []game.DifficultyDef{{ID: "normal", Name: "Normal"}}, } - if w < 20 { - t.Fatalf("log viewport width=%d, too narrow to read", w) + h := ui.RenderStatusHeader(s, p, 100) + for _, want := range []string{"Outpost 404", "Day 18", "Standard Landing", "Normal"} { + if !strings.Contains(h, want) { + t.Fatalf("missing %q in %q", want, h) + } } } From 123b34ee3eeab2795d5f99c7a876eac24f006939 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Fri, 22 May 2026 01:35:42 -0400 Subject: [PATCH 2/2] Redesign site with cinematic glitch terminal aesthetic. New hero: VT323 display font at ~9rem with dual-channel RGB glitch (red + cyan clip offset), periodic title flicker, animated vital bars (power/food/ morale/beacon), corner bracket accents, CRT scanlines, and noise overlay. Sections use left-rule log-entry layout with amber [TAG.LABEL] markers and scroll-reveal via IntersectionObserver. Assisted-by: Crush:claude-sonnet-4-5 --- docs/site/index.html | 175 +++++++----- docs/site/release.js | 12 + docs/site/style.css | 628 +++++++++++++++++++++++++++++++++---------- 3 files changed, 600 insertions(+), 215 deletions(-) diff --git a/docs/site/index.html b/docs/site/index.html index 25a2325..ba6dd5b 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -11,91 +11,116 @@ -
-
-

- Go · Charm · Bubble Tea · … -

-

OUTPOST 404

-

- Remote survival colony ops in your terminal. Balance scarce resources, survive random - events, and complete the Signal Beacon before the outpost collapses. +

+ + +
+
+

// SIGNAL INTERCEPTED — COLONY DESIGNATION 404

+

OUTPOST 404

+

+ Remote survival colony ops in your terminal.
+ Balance scarce resources, survive random events,
+ and complete the Signal Beacon before the outpost collapses.

-
- Download - Install - Source on GitHub +
+
+ PWR + +
+
+ FOOD + +
+
+ MORALE + +
+
+ BEACON + +
-
+ +

+ RELEASE … + // Go · Charm · Bubble Tea +

+
+ + + +
-
-

Latest release

-
-
Loading release…
+
+

[SYS.RELEASE] Latest Binary

+
+
Querying release feed…
-
-

Mission

-
-
- Win +
+

[OPS.BRIEF] Mission Parameters

+
+
+

WIN CONDITIONS

    -
  • Survive 45 days — reach day 46 with vitals above zero
  • -
  • Signal Beacon — complete 5 parts before collapse
  • +
  • Survive 45 days — reach day 46 with vitals above zero
  • +
  • Signal Beacon — complete all 5 parts before collapse
-
- Lose +
+

COLLAPSE TRIGGERS

    -
  • Power, food, morale, or population hits zero
  • -
  • Collapse is checked before a survival win on the final day
  • +
  • Power, food, morale, or population reaches zero
  • +
  • Collapse is checked before survival win on the final day
-
-

Install

-
-

Homebrew

+
+

[CMD.INSTALL] Deployment

+
+

Homebrew

brew install ericdahl-dev/tap/outpost-404
-

Go

+

Go toolchain

go install github.com/ericdahl-dev/outpost-404/cmd/outpost@latest
-

From source

+

From source

git clone https://github.com/ericdahl-dev/outpost-404.git
 cd outpost-404
 go run ./cmd/outpost
-
-

Run

-
-
$ outpost
-
# fixed seed for reproducible events
-
$ outpost -seed 42
-
# optional JSONL session log
-
$ outpost -log ./logs/my-run.jsonl
+
+

[CMD.EXEC] Launch

+
+
+
$ outpost
+
# fixed seed for reproducible events
+
$ outpost -seed 42
+
# optional JSONL session log
+
$ outpost -log ./logs/my-run.jsonl
+
-
-

Controls

-
+
+

[SYS.INPUT] Controls

+
@@ -110,7 +135,7 @@

Controls

- + @@ -118,43 +143,45 @@

Controls

-
-

Headless tools

-
+
+

[DEV.TOOLS] Headless Operations

+

Balance scripts and session replay for tuning and regression checks:

-
outpost -simulate scripts/conservative.json -seed 99
-outpost -simulate scripts/survival_45.json -seeds 1,7,42
-outpost -replay ./logs/my-run.jsonl
-

- See the repo - balance guide +

+
$ outpost -simulate scripts/conservative.json -seed 99
+
$ outpost -simulate scripts/survival_45.json -seeds 1,7,42
+
$ outpost -replay ./logs/my-run.jsonl
+
+

+ See the + balance guide for reference seeds and baseline expectations.

-
-

Stack

-
+
+

[SYS.ARCH] Stack

+

Built with the - Charm stack: Bubble Tea, - Lip Gloss, and Bubbles. Game rules live in internal/game; the TUI in - internal/ui. Content is JSON-driven (data/buildings.json, - data/events.json). + Charm stack: + Bubble Tea, Lip Gloss, and Bubbles. Game rules live in + internal/game; the TUI in internal/ui. + Content is JSON-driven — data/buildings.json, + data/events.json.

-
+ + diff --git a/docs/site/release.js b/docs/site/release.js index 6d98b98..23c63b9 100644 --- a/docs/site/release.js +++ b/docs/site/release.js @@ -130,4 +130,16 @@ } loadLatestRelease(); + + var entries = document.querySelectorAll('.log-entry'); + if ('IntersectionObserver' in window) { + var io = new IntersectionObserver(function(records) { + records.forEach(function(r) { + if (r.isIntersecting) { r.target.classList.add('visible'); io.unobserve(r.target); } + }); + }, { threshold: 0.08 }); + entries.forEach(function(el) { io.observe(el); }); + } else { + entries.forEach(function(el) { el.classList.add('visible'); }); + } })(); diff --git a/docs/site/style.css b/docs/site/style.css index 4a4cd3f..6d0bd8a 100644 --- a/docs/site/style.css +++ b/docs/site/style.css @@ -1,265 +1,611 @@ -:root { - --bg: #0c0f0a; - --panel: #141a12; - --border: #2a3526; - --text: #c8d4bc; - --muted: #7a8a72; - --accent: #9bdc6a; - --warn: #e8b86a; - --danger: #e07a6a; - --mono: "IBM Plex Mono", "SF Mono", "Menlo", "Consolas", monospace; - --sans: "IBM Plex Sans", system-ui, sans-serif; -} - -* { - box-sizing: border-box; -} - -html { - scroll-behavior: smooth; -} +/* ============================================================ + OUTPOST 404 — site styles + Aesthetic: cinematic glitch / CRT terminal / distress signal + ============================================================ */ +/* -- Design tokens ------------------------------------------ */ +:root { + --bg: #060806; + --bg2: #0b0f0a; + --panel: #0f140d; + --border: #1e2b1a; + --border-hi: #2e4228; + --text: #b8cdb0; + --muted: #5e7256; + --dim: #3a4e35; + --accent: #7ddc50; + --accent-hi: #aaff77; + --warn: #e8b044; + --danger: #dd5e4a; + --beacon: #50c8dd; + --mono: "JetBrains Mono", "Share Tech Mono", "SF Mono", "Menlo", monospace; + --display: "VT323", "Share Tech Mono", monospace; +} + +/* -- Reset -------------------------------------------------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { scroll-behavior: smooth; } + +/* -- Base --------------------------------------------------- */ body { - margin: 0; - min-height: 100vh; background: var(--bg); color: var(--text); - font-family: var(--sans); - font-size: 1.05rem; - line-height: 1.6; + font-family: var(--mono); + font-size: 0.95rem; + line-height: 1.7; + overflow-x: hidden; } -body::before { - content: ""; +a { color: var(--accent); text-underline-offset: 3px; } +a:hover { color: var(--accent-hi); } + +code { + font-family: var(--mono); + font-size: 0.88em; + color: var(--warn); + background: rgba(232,176,68,0.08); + padding: 0.1em 0.35em; + border-radius: 2px; +} + +/* -- Atmosphere layers -------------------------------------- */ +.scanlines { + pointer-events: none; position: fixed; inset: 0; - pointer-events: none; + z-index: 9999; background: repeating-linear-gradient( 0deg, transparent, - transparent 2px, - rgba(0, 0, 0, 0.12) 2px, - rgba(0, 0, 0, 0.12) 4px + transparent 3px, + rgba(0,0,0,0.18) 3px, + rgba(0,0,0,0.18) 4px ); - opacity: 0.35; + mix-blend-mode: multiply; } -a { - color: var(--accent); +.noise { + pointer-events: none; + position: fixed; + inset: 0; + z-index: 9998; + opacity: 0.028; + 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"); + background-size: 256px 256px; + animation: noise-drift 8s steps(10) infinite; } -a:hover { - color: #c8f0a8; +@keyframes noise-drift { + 0% { background-position: 0 0; } + 25% { background-position: 40px -30px; } + 50% { background-position: -20px 50px; } + 75% { background-position: 60px 20px; } + 100% { background-position: 0 0; } } -.wrap { +/* -- HERO --------------------------------------------------- */ +.hero { position: relative; - max-width: 52rem; - margin: 0 auto; - padding: 2.5rem 1.5rem 4rem; + min-height: 100svh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem 5rem; + background: + radial-gradient(ellipse 80% 60% at 50% 40%, rgba(60,100,40,0.18) 0%, transparent 70%), + var(--bg); + overflow: hidden; +} + +.hero::before, +.hero::after { + content: ""; + position: absolute; + width: 120px; + height: 120px; + opacity: 0.25; + animation: bracket-pulse 4s ease-in-out infinite; +} +.hero::before { + top: 2rem; left: 2rem; + border-top: 1px solid var(--accent); + border-left: 1px solid var(--accent); + animation-delay: 0s; +} +.hero::after { + bottom: 2rem; right: 2rem; + border-bottom: 1px solid var(--accent); + border-right: 1px solid var(--accent); + animation-delay: 2s; } -header { - border: 1px solid var(--border); - background: var(--panel); - padding: 1.75rem 1.5rem; - margin-bottom: 2rem; +@keyframes bracket-pulse { + 0%, 100% { opacity: 0.15; } + 50% { opacity: 0.45; } +} + +.hero-inner { + position: relative; + z-index: 1; + text-align: center; + max-width: 700px; + width: 100%; } -h1 { - margin: 0 0 0.35rem; +.hero-eyebrow { font-family: var(--mono); - font-size: clamp(1.6rem, 5vw, 2.2rem); - font-weight: 600; - letter-spacing: 0.04em; + font-size: 0.7rem; + letter-spacing: 0.18em; + color: var(--muted); + margin-bottom: 1.5rem; + animation: fade-up 0.6s ease both; +} + +/* -- GLITCH TITLE ------------------------------------------- */ +.hero-title { + font-family: var(--display); + font-size: clamp(4.5rem, 16vw, 9rem); + font-weight: 400; + line-height: 1; + letter-spacing: 0.06em; color: var(--accent); + position: relative; + display: inline-block; + margin-bottom: 1.75rem; + text-shadow: + 0 0 40px rgba(125,220,80,0.35), + 0 0 80px rgba(125,220,80,0.12); + animation: fade-up 0.6s 0.1s ease both, flicker 12s 3s infinite; +} + +.hero-title::before, +.hero-title::after { + content: attr(data-text); + position: absolute; + inset: 0; + pointer-events: none; +} + +.hero-title::before { + color: var(--danger); + clip-path: polygon(0 20%, 100% 20%, 100% 40%, 0 40%); + animation: glitch-r 5s 1.2s steps(1) infinite; + opacity: 0; +} + +.hero-title::after { + color: var(--beacon); + clip-path: polygon(0 55%, 100% 55%, 100% 70%, 0 70%); + animation: glitch-b 5s 1.8s steps(1) infinite; + opacity: 0; +} + +@keyframes glitch-r { + 0%, 90%, 100% { opacity: 0; transform: none; } + 91% { opacity: 1; transform: translate(-3px, 0) skewX(-2deg); } + 93% { opacity: 1; transform: translate(3px, 1px) skewX(1deg); } + 95% { opacity: 0; } +} + +@keyframes glitch-b { + 0%, 88%, 100% { opacity: 0; transform: none; } + 89% { opacity: 1; transform: translate(4px, -1px) skewX(1deg); } + 92% { opacity: 1; transform: translate(-2px, 0); } + 94% { opacity: 0; } +} + +@keyframes flicker { + 0%, 97%, 100% { opacity: 1; } + 97.5% { opacity: 0.6; } + 98% { opacity: 1; } + 98.4% { opacity: 0.4; } + 98.8% { opacity: 1; } } -.tagline { - margin: 0 0 1.25rem; +/* -- Hero copy ---------------------------------------------- */ +.hero-tagline { + font-family: var(--mono); + font-size: 0.88rem; + line-height: 1.9; color: var(--muted); - font-size: 1rem; + margin-bottom: 2.5rem; + animation: fade-up 0.6s 0.2s ease both; } -.cta { +/* -- Vital bars --------------------------------------------- */ +.hero-vitals { display: flex; flex-wrap: wrap; - gap: 0.75rem; + justify-content: center; + gap: 0.6rem 1.5rem; + margin-bottom: 2.5rem; + animation: fade-up 0.6s 0.3s ease both; +} + +.vital { + display: flex; + align-items: center; + gap: 0.55rem; + font-size: 0.7rem; + letter-spacing: 0.12em; +} + +.vital-label { + color: var(--muted); + min-width: 3.5rem; + text-align: right; +} + +.vital-bar { + width: 90px; + height: 6px; + background: var(--border); + position: relative; + overflow: hidden; +} + +.vital-bar::after { + content: ""; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 90deg, + transparent, + transparent 3px, + rgba(0,0,0,0.4) 3px, + rgba(0,0,0,0.4) 4px + ); +} + +.vital-fill { + display: block; + height: 100%; + width: var(--pct, 50%); + background: var(--accent); + animation: bar-fill 1.2s cubic-bezier(0.16,1,0.3,1) both; + transform-origin: left; +} + +.vital-bar--warn .vital-fill { background: var(--warn); } +.vital-bar--beacon .vital-fill { background: var(--beacon); } + +@keyframes bar-fill { + from { transform: scaleX(0); } + to { transform: scaleX(1); } +} + +/* -- Hero CTA ----------------------------------------------- */ +.hero-cta { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.65rem; + margin-bottom: 2rem; + animation: fade-up 0.6s 0.4s ease both; } .btn { display: inline-block; font-family: var(--mono); - font-size: 0.9rem; - padding: 0.55rem 1rem; - border: 1px solid var(--border); - background: #1a2218; - color: var(--accent); + font-size: 0.8rem; + letter-spacing: 0.08em; + padding: 0.55rem 1.1rem; + border: 1px solid var(--border-hi); + background: transparent; + color: var(--muted); text-decoration: none; + transition: color 0.15s, border-color 0.15s, background 0.15s, text-shadow 0.15s; } .btn:hover { + color: var(--accent); border-color: var(--accent); - background: #243020; + background: rgba(125,220,80,0.06); + text-shadow: 0 0 12px rgba(125,220,80,0.5); } .btn-primary { border-color: var(--accent); - background: #243020; + color: var(--accent); + background: rgba(125,220,80,0.07); } -section { - margin-bottom: 2rem; +.btn-primary:hover { + background: rgba(125,220,80,0.14); + text-shadow: 0 0 16px rgba(125,220,80,0.7); +} + +.hero-version { + font-size: 0.7rem; + color: var(--dim); + letter-spacing: 0.1em; + animation: fade-up 0.6s 0.5s ease both; } -h2 { +.version-tag { color: var(--accent); } + +.hero-scroll-hint { + position: absolute; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); font-family: var(--mono); - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--warn); - margin: 0 0 0.75rem; - padding-bottom: 0.35rem; - border-bottom: 1px solid var(--border); + font-size: 0.75rem; + color: var(--dim); + animation: scroll-bob 2.5s ease-in-out infinite; } -.panel { - border: 1px solid var(--border); - background: var(--panel); - padding: 1.25rem 1.5rem; +@keyframes scroll-bob { + 0%, 100% { transform: translateX(-50%) translateY(0); opacity: 0.4; } + 50% { transform: translateX(-50%) translateY(6px); opacity: 0.9; } } -pre, -.terminal { - font-family: var(--mono); - font-size: 0.85rem; - background: #080a08; - border: 1px solid var(--border); - padding: 1rem 1.1rem; - overflow-x: auto; - margin: 0.75rem 0 0; +@keyframes fade-up { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } } -.terminal .prompt { - color: var(--accent); - user-select: none; +/* -- Main content ------------------------------------------- */ +.wrap { + max-width: 56rem; + margin: 0 auto; + padding: 4rem 1.5rem 6rem; +} + +/* -- Log entry sections ------------------------------------- */ +.log-entry { + margin-bottom: 3.5rem; + position: relative; + padding-left: 1.5rem; + border-left: 2px solid var(--border); + transition: border-color 0.3s; + opacity: 0; + transform: translateY(16px); + transition: opacity 0.55s ease, transform 0.55s ease, border-color 0.3s; +} + +.log-entry:hover { + border-left-color: var(--border-hi); } -.terminal .dim { +.log-entry.visible { + opacity: 1; + transform: none; +} + +.log-label { + font-family: var(--mono); + font-size: 0.72rem; + letter-spacing: 0.16em; + font-weight: 600; + text-transform: uppercase; color: var(--muted); + margin-bottom: 1.25rem; + display: flex; + align-items: center; + gap: 0.65rem; } -.grid { +.log-tag { + color: var(--warn); + letter-spacing: 0.08em; +} + +/* -- Mission blocks ----------------------------------------- */ +.log-grid { display: grid; gap: 1rem; } @media (min-width: 640px) { - .grid-2 { - grid-template-columns: 1fr 1fr; - } + .log-grid { grid-template-columns: 1fr 1fr; } } -table { - width: 100%; - border-collapse: collapse; - font-size: 0.95rem; +.mission-block { + background: var(--panel); + border: 1px solid var(--border); + padding: 1.25rem; } -th, -td { - text-align: left; - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border); +.mission-block ul { + list-style: none; + margin-top: 0.65rem; } -th { - font-family: var(--mono); - font-size: 0.8rem; - color: var(--muted); - font-weight: 500; +.mission-block li { + padding: 0.3rem 0 0.3rem 1.1rem; + position: relative; + font-size: 0.88rem; } -td:first-child { +.mission-win li::before { content: "+"; position: absolute; left: 0; color: var(--accent); } +.mission-lose li::before { content: "!"; position: absolute; left: 0; color: var(--danger); } + +.mission-header { font-family: var(--mono); - color: var(--warn); + font-size: 0.7rem; + letter-spacing: 0.14em; + font-weight: 600; } -ul { - margin: 0.5rem 0 0; - padding-left: 1.25rem; +.mission-win .mission-header { color: var(--accent); } +.mission-lose .mission-header { color: var(--danger); } + +/* -- Install methods ---------------------------------------- */ +.install-method { + font-size: 0.72rem; + letter-spacing: 0.12em; + color: var(--muted); + margin: 1rem 0 0.35rem; } -li { - margin-bottom: 0.35rem; +.install-method:first-of-type { margin-top: 0; } + +/* -- Preformatted code -------------------------------------- */ +pre { + font-family: var(--mono); + font-size: 0.82rem; + background: #020402; + border: 1px solid var(--border); + border-left: 3px solid var(--border-hi); + padding: 0.9rem 1.1rem; + overflow-x: auto; + line-height: 1.65; + color: var(--text); } -footer { - margin-top: 3rem; - padding-top: 1.5rem; - border-top: 1px solid var(--border); - font-size: 0.9rem; - color: var(--muted); +pre code { + background: none; + padding: 0; + color: inherit; + font-size: inherit; } -.badge { - display: inline-block; +/* -- Terminal block ----------------------------------------- */ +.terminal { font-family: var(--mono); - font-size: 0.75rem; - padding: 0.15rem 0.45rem; + font-size: 0.82rem; + background: #020402; border: 1px solid var(--border); - color: var(--muted); - margin-right: 0.35rem; + border-left: 3px solid var(--accent); + padding: 0.9rem 1.1rem; + line-height: 1.9; + overflow-x: auto; + position: relative; } -.version-pill { +.terminal::before { + content: "\25CF \25CF \25CF"; + display: block; + color: var(--dim); + font-size: 0.6rem; + letter-spacing: 0.3rem; + margin-bottom: 0.7rem; +} + +.prompt { color: var(--accent); - border-color: var(--accent); - margin-left: 0.25rem; + user-select: none; +} + +/* -- Table -------------------------------------------------- */ +table { + width: 100%; + border-collapse: collapse; + font-size: 0.87rem; +} + +th, td { + text-align: left; + padding: 0.55rem 0.85rem; + border-bottom: 1px solid var(--border); +} + +th { + font-family: var(--mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--dim); + font-weight: 400; +} + +td:first-child { + font-family: var(--mono); + color: var(--warn); + width: 7rem; } +tr:hover td { color: var(--text); } +tr:hover td:first-child { color: var(--accent-hi); } + +/* -- Release assets ----------------------------------------- */ .release-title { - margin: 0 0 0.5rem; + color: var(--text); + font-size: 0.9rem; + margin-bottom: 0.25rem; } .asset-list { list-style: none; - margin: 0.75rem 0 0; - padding: 0; + margin: 0.85rem 0 0; display: grid; - gap: 0.5rem; + gap: 0.4rem; } .asset-list a { display: block; font-family: var(--mono); - font-size: 0.9rem; - padding: 0.5rem 0.75rem; + font-size: 0.82rem; + padding: 0.5rem 0.85rem; border: 1px solid var(--border); - background: #080a08; + background: #020402; text-decoration: none; + color: var(--text); + transition: border-color 0.15s, color 0.15s; } .asset-list a:hover { border-color: var(--accent); + color: var(--accent); } .release-notes { margin-top: 1.25rem; - font-size: 0.95rem; + font-size: 0.88rem; color: var(--muted); } .release-notes h3 { font-family: var(--mono); - font-size: 0.85rem; - letter-spacing: 0.08em; + font-size: 0.7rem; + letter-spacing: 0.14em; text-transform: uppercase; color: var(--warn); margin: 0 0 0.5rem; } .release-notes ul { - padding-left: 1.25rem; + list-style: none; + padding: 0; +} + +.release-notes li { + padding: 0.2rem 0 0.2rem 1rem; + position: relative; +} + +.release-notes li::before { + content: "\2192"; + position: absolute; + left: 0; + color: var(--dim); +} + +/* -- Utilities ---------------------------------------------- */ +.dim { color: var(--muted); } +.small { font-size: 0.82rem; margin-top: 0.75rem; } + +/* -- Footer ------------------------------------------------- */ +footer { + border-top: 1px solid var(--border); + padding-top: 1.5rem; + margin-top: 2rem; + font-size: 0.78rem; + color: var(--dim); + text-align: center; + letter-spacing: 0.05em; +} + +footer a { color: var(--muted); } +footer a:hover { color: var(--accent); } + +/* -- Responsive --------------------------------------------- */ +@media (max-width: 480px) { + .hero-vitals { gap: 0.5rem 1rem; } + .vital-bar { width: 70px; } + .hero-cta { gap: 0.5rem; } + .wrap { padding: 2.5rem 1rem 4rem; } + .log-entry { padding-left: 0.9rem; } }
rRepair systems
tTrade food for credits
sWork on Signal Beacon
n / spaceNext day
n / spaceAdvance to next day
?Help
qQuit