diff --git a/landing/design/.thumbnail b/landing/design/.thumbnail new file mode 100644 index 0000000..91b3354 Binary files /dev/null and b/landing/design/.thumbnail differ diff --git a/landing/design/Ulicraft Landing.html b/landing/design/Ulicraft Landing.html new file mode 100644 index 0000000..a9233e0 --- /dev/null +++ b/landing/design/Ulicraft Landing.html @@ -0,0 +1,22 @@ + + + + + + Ulicraft — Java Survival SMP + + + + + + +
+ + + + + + + + + diff --git a/landing/design/app.jsx b/landing/design/app.jsx new file mode 100644 index 0000000..997cb53 --- /dev/null +++ b/landing/design/app.jsx @@ -0,0 +1,434 @@ +// Ulicraft landing — pixel components, live status, copy-IP, scroll reveals, tweaks +const { useState, useEffect, useRef, useCallback } = React; + +/* ---------- pixel art helpers ---------- */ +// 8x8 creeper face +const CREEPER = [ + "00000000", + "01100110", + "01100110", + "00011000", + "00111100", + "00111100", + "00100100", + "00000000", +]; +function Creeper() { + const cells = CREEPER.join("").split(""); + return ( + + ); +} + +// 7x7 feature glyphs +const GLYPHS = { + shield: ["0111110","1111111","1111111","1111111","0111110","0011100","0001000"], + diamond:["0001000","0011100","0111110","1111111","0111110","0011100","0001000"], + orb: ["0011100","0111110","1111111","1111111","1111111","0111110","0011100"], + heart: ["0110110","1111111","1111111","1111111","0111110","0011100","0001000"], +}; +function PixelIcon({ glyph, gold }) { + const cells = GLYPHS[glyph].join("").split(""); + return ( + + ); +} + +/* ---------- copy to clipboard ---------- */ +function copyText(text) { + try { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text); + return; + } + } catch (e) {} + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (e) {} + document.body.removeChild(ta); +} + +function IpChip({ ip }) { + const [copied, setCopied] = useState(false); + const onCopy = () => { + copyText(ip); + setCopied(true); + clearTimeout(onCopy._t); + onCopy._t = setTimeout(() => setCopied(false), 1600); + }; + return ( +
+ {ip} + +
+ ); +} + +/* ---------- live player count ---------- */ +function useLivePlayers(base = 37, max = 100) { + const [n, setN] = useState(base); + useEffect(() => { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + const id = setInterval(() => { + setN((p) => { + const step = Math.floor(Math.random() * 5) - 2; + return Math.max(base - 6, Math.min(max - 4, p + step)); + }); + }, 2600); + return () => clearInterval(id); + }, [base, max]); + return n; +} + +/* ---------- floating pixel dust ---------- */ +function Dust({ on }) { + if (!on) return null; + const bits = Array.from({ length: 16 }, (_, i) => i); + return ( + + ); +} + +/* ---------- server-list status panel ---------- */ +function ServerListPanel({ players, max, version }) { + return ( +
+
+
+
+ Ulicraft + Java {version} +
+

+ ⛏ Season 3 · Survival SMP · whitelist open +

+
+
+
+ + {players}/{max} +
+
+
+ ); +} + +/* ---------- scroll reveal (fail-safe: visible at rest, .anim added when in view) ---------- */ +function useReveal() { + useEffect(() => { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + let els = [...document.querySelectorAll(".reveal")]; + let stop = false; + const tick = () => { + if (stop) return; + const vh = window.innerHeight; + for (let i = els.length - 1; i >= 0; i--) { + const r = els[i].getBoundingClientRect(); + if (r.top < vh * 0.9 && r.bottom > 0) { + els[i].classList.add("anim"); + els.splice(i, 1); + } + } + if (els.length) requestAnimationFrame(tick); + else stop = true; + }; + requestAnimationFrame(tick); + return () => { stop = true; }; + }, []); +} + +/* ============================================================ */ +const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ + "heroLayout": "centered", + "mood": "grass", + "headFont": "pixelify", + "serverIp": "play.ulicraft.net", + "tagline": "Hardcore survival, built to last.", + "particles": true +}/*EDITMODE-END*/; + +const HEAD_FONTS = { + pixelify: "'Pixelify Sans', system-ui, sans-serif", + "8bit": "'Press Start 2P', monospace", + silkscreen: "'Silkscreen', system-ui, sans-serif", +}; + +const FEATURES = [ + { glyph: "shield", gold: false, h: "Grief-proof claims", + p: "Lock down your base with golden-shovel land claims. Your builds stay yours — even while you're offline." }, + { glyph: "diamond", gold: true, h: "Zero pay-to-win", + p: "The store sells cosmetics and nothing else. Every diamond is earned in-game, never bought." }, + { glyph: "orb", gold: false, h: "A living world", + p: "Seasonal map resets, custom bosses, and community events keep the overworld worth logging into." }, + { glyph: "heart", gold: true, h: "Real community", + p: "A whitelisted, moderated server with an active Discord. Toxicity meets the ban hammer, fast." }, +]; + +function App() { + const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); + const players = useLivePlayers(37, 100); + const peak = 84; + const version = "1.21.4"; + + // guarantee pixel head fonts are downloaded before first paint settles + useEffect(() => { + if (!document.fonts || !document.fonts.load) return; + ["600 32px 'Pixelify Sans'", "700 32px 'Pixelify Sans'", + "400 32px 'Press Start 2P'", "400 32px 'Silkscreen'", "700 32px 'Silkscreen'"] + .forEach((f) => document.fonts.load(f).catch(() => {})); + }, []); + + // apply mood / hero / font to + useEffect(() => { + const r = document.documentElement; + r.setAttribute("data-mood", t.mood); + r.setAttribute("data-hero", t.heroLayout); + r.setAttribute("data-head", t.headFont); + r.style.setProperty("--font-head", HEAD_FONTS[t.headFont] || HEAD_FONTS.pixelify); + }, [t.mood, t.heroLayout, t.headFont]); + + useReveal(); + + return ( + <> + {/* NAV */} +
+ +
+ + {/* HERO */} +
+
+ +
+
+ Ulicraft +
+
+ Java Edition · Survival SMP · Season 3 +

{t.tagline}

+

+ A whitelisted Java SMP with land claims, seasonal events, and zero pay-to-win. + Grab the IP, fire up your launcher, and stake your claim. +

+
+ + How to Join +
+
+ + + {players} playing now + + Java {version} + + No mods required +
+
+
+ +
+
+
+ + {/* STATUS */} +
+
+
+ Server Status +

Live, and waiting for you.

+

This is exactly what you'll see in your multiplayer list.

+
+
+ +
+
+ {[ + { k: <>{players}/100, l: "Players Online" }, + { k: <>{peak}, l: "Peak Today" }, + { k: <>99.9%, l: "Uptime" }, + { k: <>20.0 tps, l: "Performance" }, + ].map((s, i) => ( +
+
{s.k}
+
{s.l}
+
+ ))} +
+
+
+ + {/* FEATURES */} +
+
+
+ What makes it Ulicraft +

Built for players who stay.

+

No reset roulette, no whales buying god gear. Just a server that respects your time.

+
+
+ {FEATURES.map((f, i) => ( +
+ +
+

{f.h}

+

{f.p}

+
+
+ ))} +
+
+
+ + {/* HOW TO JOIN */} +
+
+
+ How to Join · 3 Steps +

From zero to spawning in.

+

Ulicraft runs on Minecraft: Java Edition. Here's the whole path, start to finish.

+
+ +
+ {/* STEP 1 */} +
+
1
+
+ Account +

Get Minecraft: Java Edition

+

+ Create a free Microsoft account, then buy and download Minecraft: Java & Bedrock + Edition from minecraft.net. Already own Java Edition? Skip straight to step 2. +

+
+ Get Java Edition → + Microsoft account required +
+
+
+ + {/* STEP 2 */} +
+
2
+
+ Launcher +

Download & set up the launcher

+

+ Install the official Minecraft Launcher, sign in with your Microsoft account, and select + the Latest Release ({version}) profile. +

+
    +
  • Download the launcher for Windows, macOS, or Linux.
  • +
  • Sign in → pick Latest Release → press Play once to finish installing.
  • +
  • Optional: allocate 4–6 GB RAM under Installations → More Options for smoother play.
  • +
+
+ Download launcher → + No mods or modpack needed +
+
+
+ + {/* STEP 3 */} +
+
3
+
+ Connect +

Join the Ulicraft server

+

+ In Minecraft, open MultiplayerAdd Server. Name it Ulicraft, + paste the address below, hit Done, then double-click the server to join. +

+
+ +
+ Whitelisted server — apply in our Discord first to get added. +
+
+
+
+
+
+ + {/* FOOTER */} + + + {/* TWEAKS */} + + + setTweak("heroLayout", v)} /> + + setTweak("mood", v)} /> + + setTweak("headFont", v)} /> + + setTweak("tagline", v)} /> + setTweak("serverIp", v)} /> + setTweak("particles", v)} /> + + + ); +} + +ReactDOM.createRoot(document.getElementById("root")).render(); diff --git a/landing/design/assets/ulicraft-logo.png b/landing/design/assets/ulicraft-logo.png new file mode 100644 index 0000000..86dbead Binary files /dev/null and b/landing/design/assets/ulicraft-logo.png differ diff --git a/landing/design/screenshots/01-full.png b/landing/design/screenshots/01-full.png new file mode 100644 index 0000000..e34b83a Binary files /dev/null and b/landing/design/screenshots/01-full.png differ diff --git a/landing/design/screenshots/01-s.png b/landing/design/screenshots/01-s.png new file mode 100644 index 0000000..5433a55 Binary files /dev/null and b/landing/design/screenshots/01-s.png differ diff --git a/landing/design/screenshots/01-sec.png b/landing/design/screenshots/01-sec.png new file mode 100644 index 0000000..d438dcd Binary files /dev/null and b/landing/design/screenshots/01-sec.png differ diff --git a/landing/design/screenshots/01-sec2.png b/landing/design/screenshots/01-sec2.png new file mode 100644 index 0000000..d438dcd Binary files /dev/null and b/landing/design/screenshots/01-sec2.png differ diff --git a/landing/design/screenshots/01-v.png b/landing/design/screenshots/01-v.png new file mode 100644 index 0000000..8762469 Binary files /dev/null and b/landing/design/screenshots/01-v.png differ diff --git a/landing/design/screenshots/02-full.png b/landing/design/screenshots/02-full.png new file mode 100644 index 0000000..497cfdc Binary files /dev/null and b/landing/design/screenshots/02-full.png differ diff --git a/landing/design/screenshots/02-s.png b/landing/design/screenshots/02-s.png new file mode 100644 index 0000000..0959771 Binary files /dev/null and b/landing/design/screenshots/02-s.png differ diff --git a/landing/design/screenshots/02-sec.png b/landing/design/screenshots/02-sec.png new file mode 100644 index 0000000..59ef32a Binary files /dev/null and b/landing/design/screenshots/02-sec.png differ diff --git a/landing/design/screenshots/02-sec2.png b/landing/design/screenshots/02-sec2.png new file mode 100644 index 0000000..59ef32a Binary files /dev/null and b/landing/design/screenshots/02-sec2.png differ diff --git a/landing/design/screenshots/02-v.png b/landing/design/screenshots/02-v.png new file mode 100644 index 0000000..4c05352 Binary files /dev/null and b/landing/design/screenshots/02-v.png differ diff --git a/landing/design/screenshots/03-full.png b/landing/design/screenshots/03-full.png new file mode 100644 index 0000000..2c6a269 Binary files /dev/null and b/landing/design/screenshots/03-full.png differ diff --git a/landing/design/screenshots/03-s.png b/landing/design/screenshots/03-s.png new file mode 100644 index 0000000..49f6157 Binary files /dev/null and b/landing/design/screenshots/03-s.png differ diff --git a/landing/design/screenshots/03-sec.png b/landing/design/screenshots/03-sec.png new file mode 100644 index 0000000..c5b0292 Binary files /dev/null and b/landing/design/screenshots/03-sec.png differ diff --git a/landing/design/screenshots/03-sec2.png b/landing/design/screenshots/03-sec2.png new file mode 100644 index 0000000..c5b0292 Binary files /dev/null and b/landing/design/screenshots/03-sec2.png differ diff --git a/landing/design/screenshots/03-v.png b/landing/design/screenshots/03-v.png new file mode 100644 index 0000000..7c36544 Binary files /dev/null and b/landing/design/screenshots/03-v.png differ diff --git a/landing/design/screenshots/04-full.png b/landing/design/screenshots/04-full.png new file mode 100644 index 0000000..2c6a269 Binary files /dev/null and b/landing/design/screenshots/04-full.png differ diff --git a/landing/design/screenshots/04-s.png b/landing/design/screenshots/04-s.png new file mode 100644 index 0000000..e2c9d0f Binary files /dev/null and b/landing/design/screenshots/04-s.png differ diff --git a/landing/design/screenshots/04-sec.png b/landing/design/screenshots/04-sec.png new file mode 100644 index 0000000..0959771 Binary files /dev/null and b/landing/design/screenshots/04-sec.png differ diff --git a/landing/design/screenshots/04-sec2.png b/landing/design/screenshots/04-sec2.png new file mode 100644 index 0000000..0959771 Binary files /dev/null and b/landing/design/screenshots/04-sec2.png differ diff --git a/landing/design/screenshots/04-v.png b/landing/design/screenshots/04-v.png new file mode 100644 index 0000000..90e63ff Binary files /dev/null and b/landing/design/screenshots/04-v.png differ diff --git a/landing/design/screenshots/05-s.png b/landing/design/screenshots/05-s.png new file mode 100644 index 0000000..0959771 Binary files /dev/null and b/landing/design/screenshots/05-s.png differ diff --git a/landing/design/screenshots/06-s.png b/landing/design/screenshots/06-s.png new file mode 100644 index 0000000..b215903 Binary files /dev/null and b/landing/design/screenshots/06-s.png differ diff --git a/landing/design/screenshots/hero-hq.png b/landing/design/screenshots/hero-hq.png new file mode 100644 index 0000000..0466240 Binary files /dev/null and b/landing/design/screenshots/hero-hq.png differ diff --git a/landing/design/styles.css b/landing/design/styles.css new file mode 100644 index 0000000..6ce0a7b --- /dev/null +++ b/landing/design/styles.css @@ -0,0 +1,579 @@ +/* ============================================================ + ULICRAFT — "Carved from stone" + Design system: dark overworld, MC-GUI bevels, pixel type + ============================================================ */ + +/* ---- Mood palettes (data-mood on ) ---- */ +:root, +:root[data-mood="grass"] { + --bg: oklch(0.165 0.014 150); + --bg-2: oklch(0.205 0.016 150); + --surface: oklch(0.235 0.017 150); + --surface-2: oklch(0.285 0.018 150); + --slot: oklch(0.185 0.014 150); + --bevel-hi: oklch(0.40 0.018 150); + --bevel-lo: oklch(0.115 0.012 150); + --line: oklch(0.33 0.016 150); + + --accent: oklch(0.72 0.16 145); /* grass */ + --accent-hi: oklch(0.80 0.15 145); + --accent-lo: oklch(0.55 0.15 145); + --accent-ink: oklch(0.17 0.05 150); /* text on accent */ + + --gold: oklch(0.82 0.135 85); + --glow: oklch(0.72 0.16 145 / 0.30); + + --text: oklch(0.95 0.008 110); + --muted: oklch(0.72 0.012 145); + --dim: oklch(0.55 0.012 145); +} + +:root[data-mood="nether"] { + --bg: oklch(0.165 0.018 35); + --bg-2: oklch(0.205 0.022 32); + --surface: oklch(0.235 0.026 32); + --surface-2: oklch(0.285 0.030 32); + --slot: oklch(0.185 0.020 32); + --bevel-hi: oklch(0.42 0.040 35); + --bevel-lo: oklch(0.115 0.016 32); + --line: oklch(0.34 0.030 32); + + --accent: oklch(0.64 0.19 32); /* nether red */ + --accent-hi: oklch(0.72 0.18 38); + --accent-lo: oklch(0.50 0.17 30); + --accent-ink: oklch(0.16 0.04 32); + + --gold: oklch(0.83 0.14 75); + --glow: oklch(0.64 0.19 32 / 0.34); + + --text: oklch(0.95 0.010 60); + --muted: oklch(0.74 0.020 45); + --dim: oklch(0.56 0.020 40); +} + +:root[data-mood="end"] { + --bg: oklch(0.155 0.018 305); + --bg-2: oklch(0.195 0.022 305); + --surface: oklch(0.225 0.026 305); + --surface-2: oklch(0.275 0.030 305); + --slot: oklch(0.175 0.020 305); + --bevel-hi: oklch(0.42 0.040 305); + --bevel-lo: oklch(0.110 0.016 305); + --line: oklch(0.34 0.030 305); + + --accent: oklch(0.74 0.135 175); /* end teal */ + --accent-hi: oklch(0.82 0.13 175); + --accent-lo: oklch(0.58 0.13 178); + --accent-ink: oklch(0.16 0.04 200); + + --gold: oklch(0.84 0.13 95); + --glow: oklch(0.70 0.16 300 / 0.34); + + --text: oklch(0.96 0.010 300); + --muted: oklch(0.76 0.020 300); + --dim: oklch(0.58 0.020 300); +} + +/* ---- Fonts (switchable head face via --font-head) ---- */ +:root { + --font-head: 'Pixelify Sans', system-ui, sans-serif; + --font-body: 'Space Grotesk', system-ui, sans-serif; + --font-pixel: 'Press Start 2P', monospace; /* tiny eyebrow labels */ + --font-mono: 'VT323', monospace; /* IP / terminal */ + --maxw: 1160px; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-body); + font-size: 17px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +/* faint pixel-grid stone texture over everything */ +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: + repeating-linear-gradient(0deg, oklch(1 0 0 / 0.018) 0 1px, transparent 1px 4px), + repeating-linear-gradient(90deg, oklch(0 0 0 / 0.06) 0 1px, transparent 1px 4px); + background-size: 4px 4px, 4px 4px; + mix-blend-mode: overlay; + opacity: 0.5; +} + +img { display: block; max-width: 100%; } + +::selection { background: var(--accent); color: var(--accent-ink); } + +/* ---- Typography helpers ---- */ +.eyebrow { + font-family: var(--font-pixel); + font-size: clamp(9px, 1.1vw, 11px); + letter-spacing: 0.12em; + color: var(--accent); + text-transform: uppercase; + line-height: 1.8; +} +h1, h2, h3 { + font-family: var(--font-head); + font-weight: 600; + line-height: 1.02; + letter-spacing: 0.01em; + margin: 0; +} +.section-title { + font-size: clamp(34px, 5vw, 58px); +} +.lead { color: var(--muted); } +.mono { font-family: var(--font-mono); } + +/* ---- Layout ---- */ +.wrap { width: min(var(--maxw), calc(100% - 48px)); margin-inline: auto; } +section { position: relative; z-index: 1; } +.pad { padding-block: clamp(64px, 9vw, 130px); } +.sec-head { max-width: 640px; margin-bottom: 48px; } +.sec-head .eyebrow { display: block; margin-bottom: 14px; } +.sec-head p { margin: 16px 0 0; font-size: 18px; } + +/* ============================================================ + MINECRAFT-STYLE BUTTON + ============================================================ */ +.mc-btn { + --b: var(--surface-2); + position: relative; + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--font-head); + font-size: 17px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--text); + background: var(--b); + border: 0; + padding: 15px 24px 17px; + cursor: pointer; + text-decoration: none; + box-shadow: + inset 2px 2px 0 var(--bevel-hi), + inset -2px -2px 0 var(--bevel-lo), + 0 4px 0 oklch(0 0 0 / 0.5), + 0 8px 18px oklch(0 0 0 / 0.4); + transition: transform .08s ease, filter .12s ease; + image-rendering: pixelated; + text-shadow: 0 2px 0 oklch(0 0 0 / 0.35); + white-space: nowrap; +} +.mc-btn:hover { filter: brightness(1.12); } +.mc-btn:active { + transform: translateY(4px); + box-shadow: + inset 2px 2px 0 var(--bevel-hi), + inset -2px -2px 0 var(--bevel-lo), + 0 0 0 oklch(0 0 0 / 0.5), + 0 2px 8px oklch(0 0 0 / 0.4); +} +.mc-btn.primary { + --b: var(--accent); + --bevel-hi: var(--accent-hi); + --bevel-lo: var(--accent-lo); + color: var(--accent-ink); + text-shadow: 0 2px 0 oklch(1 0 0 / 0.22); +} +.mc-btn.gold { + --b: var(--gold); + --bevel-hi: oklch(0.90 0.10 90); + --bevel-lo: oklch(0.66 0.13 75); + color: oklch(0.20 0.05 80); + text-shadow: 0 2px 0 oklch(1 0 0 / 0.25); +} +.mc-btn.sm { font-size: 14px; padding: 10px 16px 12px; } + +/* ---- Copy-IP chip ---- */ +.ip-chip { + display: inline-flex; + align-items: stretch; + background: var(--slot); + box-shadow: + inset 2px 2px 0 var(--bevel-lo), + inset -2px -2px 0 var(--bevel-hi); + overflow: hidden; +} +.ip-chip .ip-val { + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px; + font-family: var(--font-mono); + font-size: 28px; + line-height: 1; + color: var(--gold); + letter-spacing: 0.02em; +} +.ip-chip .ip-val .pin { color: var(--dim); font-size: 22px; } +.ip-chip button { + border: 0; + cursor: pointer; + padding: 14px 18px; + font-family: var(--font-head); + font-weight: 600; + font-size: 15px; + background: var(--accent); + color: var(--accent-ink); + box-shadow: inset 2px 2px 0 var(--accent-hi), inset -2px -2px 0 var(--accent-lo); + transition: filter .12s ease; + white-space: nowrap; +} +.ip-chip button:hover { filter: brightness(1.1); } +.ip-chip button.copied { background: var(--gold); color: oklch(0.20 0.05 80); } + +/* ============================================================ + NAV + ============================================================ */ +.nav { + position: sticky; + top: 0; + z-index: 50; + background: oklch(0.165 0.014 150 / 0.72); + -webkit-backdrop-filter: blur(14px) saturate(140%); + backdrop-filter: blur(14px) saturate(140%); + border-bottom: 1px solid var(--line); +} +.nav-in { + display: flex; + align-items: center; + gap: 24px; + height: 68px; +} +.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); } +.brand .name { + font-family: var(--font-head); + font-weight: 600; + font-size: 22px; + letter-spacing: 0.02em; +} +.nav-links { display: flex; gap: 6px; margin-left: 12px; } +.nav-links a { + color: var(--muted); + text-decoration: none; + font-size: 15px; + font-weight: 500; + padding: 8px 12px; + transition: color .12s ease, background .12s ease; +} +.nav-links a:hover { color: var(--text); background: var(--surface); } +.nav-spacer { flex: 1; } + +/* ---- Creeper pixel mark ---- */ +.creeper { + display: grid; + grid-template-columns: repeat(8, 1fr); + grid-template-rows: repeat(8, 1fr); + width: 34px; + height: 34px; + background: var(--accent); + box-shadow: inset 2px 2px 0 var(--accent-hi), inset -2px -2px 0 var(--accent-lo); + image-rendering: pixelated; +} +.creeper i { background: transparent; } +.creeper i.f { background: oklch(0.16 0.03 150); } + +/* ============================================================ + HERO + ============================================================ */ +.hero { + position: relative; + padding-top: clamp(48px, 7vw, 90px); + padding-bottom: clamp(56px, 8vw, 110px); + overflow: hidden; +} +.hero::before { /* spotlight glow behind logo */ + content: ""; + position: absolute; + top: -10%; + left: 50%; + width: 1100px; + height: 760px; + transform: translateX(-50%); + background: radial-gradient(closest-side, var(--glow), transparent 72%); + pointer-events: none; + filter: blur(8px); +} +.hero-grid { position: relative; display: grid; gap: clamp(36px, 5vw, 64px); align-items: center; } + +/* layout: centered (default) */ +:root[data-hero="centered"] .hero-grid { grid-template-columns: 1fr; justify-items: center; text-align: center; } +:root[data-hero="centered"] .hero-cta { justify-content: center; } +:root[data-hero="centered"] .hero-meta { justify-content: center; } +:root[data-hero="centered"] .hero-copy { max-width: 720px; } +:root[data-hero="centered"] .hero-status { display: none; } + +/* layout: split (copy left, live panel right) */ +:root[data-hero="split"] .hero-grid { grid-template-columns: 1.1fr 0.9fr; } +:root[data-hero="split"] .hero-logo { max-width: 560px; } +:root[data-hero="split"] .hero-logo img { margin-inline: 0; } + +/* layout: spotlight (giant logo, minimal text, centered) */ +:root[data-hero="spotlight"] .hero-grid { grid-template-columns: 1fr; justify-items: center; text-align: center; } +:root[data-hero="spotlight"] .hero-cta { justify-content: center; } +:root[data-hero="spotlight"] .hero-meta { justify-content: center; } +:root[data-hero="spotlight"] .hero-logo { max-width: 900px; } +:root[data-hero="spotlight"] .hero-tagline { font-size: clamp(20px, 2.4vw, 28px); } +:root[data-hero="spotlight"] .hero-status { display: none; } +:root[data-hero="spotlight"] .hero-features-note { display: none; } + +.hero-logo { width: 100%; max-width: 760px; } +.hero-logo img { + width: 100%; + image-rendering: pixelated; + filter: drop-shadow(0 8px 0 oklch(0 0 0 / 0.45)) drop-shadow(0 18px 30px oklch(0 0 0 / 0.55)); + margin-inline: auto; +} +.hero-copy { display: flex; flex-direction: column; gap: 22px; } +.hero-tagline { + font-family: var(--font-head); + font-weight: 600; + font-size: clamp(24px, 3vw, 38px); + line-height: 1.08; + letter-spacing: 0.01em; + text-wrap: balance; +} +.hero-tagline .hl { color: var(--accent); } +.hero-sub { color: var(--muted); font-size: 18px; max-width: 52ch; margin: 0; } +.hero-cta { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; } +.hero-meta { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; color: var(--dim); font-size: 14.5px; } +.hero-meta .dot { width: 6px; height: 6px; background: var(--dim); } +.hero-ip-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; } + +/* status pill */ +.live-pill { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 7px 14px 7px 11px; + background: var(--slot); + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); + font-size: 14px; + color: var(--muted); + font-weight: 500; +} +.live-dot { + width: 9px; height: 9px; background: var(--accent); + box-shadow: 0 0 0 3px var(--glow); + animation: pulse 2.2s ease-in-out infinite; +} +@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} } +@media (prefers-reduced-motion: reduce){ .live-dot{ animation:none } } +.live-pill b { color: var(--text); font-variant-numeric: tabular-nums; } + +/* ============================================================ + SERVER-LIST STATUS PANEL + ============================================================ */ +.serverlist { + display: flex; + gap: 18px; + align-items: center; + background: var(--slot); + padding: 16px; + box-shadow: + inset 2px 2px 0 var(--bevel-hi), + inset -2px -2px 0 var(--bevel-lo), + 0 8px 24px oklch(0 0 0 / 0.4); +} +.serverlist .icon { + width: 72px; height: 72px; flex-shrink: 0; + display: grid; place-items: center; + background: var(--bg-2); + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); +} +.serverlist .icon .creeper { width: 48px; height: 48px; } +.serverlist .meta { flex: 1; min-width: 0; } +.serverlist .row1 { display: flex; align-items: baseline; gap: 12px; flex-wrap: nowrap; } +.serverlist .title { font-family: var(--font-head); font-weight: 600; font-size: 22px; white-space: nowrap; } +.serverlist .ver { color: var(--dim); font-family: var(--font-mono); font-size: 17px; white-space: nowrap; } +.serverlist .motd { margin: 6px 0 0; color: var(--muted); font-size: 15px; } +.serverlist .motd .g { color: var(--gold); } +.serverlist .motd .a { color: var(--accent); } +.serverlist .stat { text-align: right; flex-shrink: 0; } +.serverlist .players { font-variant-numeric: tabular-nums; font-size: 15px; color: var(--muted); display:flex; align-items:center; gap:8px; justify-content:flex-end; } +.serverlist .players b { color: var(--text); } + +/* signal bars */ +.bars { display: inline-flex; align-items: flex-end; gap: 2px; height: 16px; } +.bars i { width: 4px; background: var(--accent); image-rendering: pixelated; } +.bars i:nth-child(1){height:25%} +.bars i:nth-child(2){height:45%} +.bars i:nth-child(3){height:65%} +.bars i:nth-child(4){height:85%} +.bars i:nth-child(5){height:100%} + +/* stat tiles */ +.stat-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-top: 28px; +} +.tile { + background: var(--surface); + padding: 22px; + box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo); +} +.tile .k { font-family: var(--font-head); font-weight: 600; font-size: 38px; color: var(--text); line-height: 1; font-variant-numeric: tabular-nums; } +.tile .k .u { color: var(--accent); font-size: 22px; } +.tile .l { margin-top: 8px; color: var(--dim); font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; font-family: var(--font-pixel); font-size: 9px; line-height: 1.8; } + +/* ============================================================ + FEATURES + ============================================================ */ +.feat-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 18px; +} +.feat { + display: flex; + gap: 18px; + background: var(--surface); + padding: 26px; + box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo); + transition: transform .14s ease, filter .14s ease; +} +.feat:hover { transform: translateY(-3px); filter: brightness(1.06); } +.feat h3 { font-size: 21px; margin-bottom: 8px; } +.feat p { margin: 0; color: var(--muted); font-size: 16px; } + +/* pixel icon */ +.picon { + width: 52px; height: 52px; flex-shrink: 0; + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: repeat(7, 1fr); + background: var(--slot); + padding: 4px; + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); + image-rendering: pixelated; +} +.picon i { background: transparent; } +.picon i.on { background: var(--accent); } +.picon i.go { background: var(--gold); } + +/* ============================================================ + HOW TO JOIN — steps + ============================================================ */ +.steps { display: flex; flex-direction: column; gap: 20px; } +.step { + display: grid; + grid-template-columns: 92px 1fr; + gap: 28px; + background: var(--surface); + padding: 30px; + box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo); +} +.step .num { + width: 92px; height: 92px; + display: grid; place-items: center; + background: var(--slot); + font-family: var(--font-head); + font-weight: 600; + font-size: 46px; + color: var(--accent); + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); +} +.step .body { padding-top: 2px; } +.step .kicker { font-family: var(--font-pixel); font-size: 9px; letter-spacing: 0.1em; color: var(--dim); text-transform: uppercase; } +.step h3 { font-size: 26px; margin: 10px 0 12px; } +.step p { margin: 0 0 16px; color: var(--muted); font-size: 16.5px; max-width: 64ch; } +.step .actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; } +.step ul { margin: 0 0 16px; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 9px; } +.step ul li { display: flex; gap: 12px; color: var(--muted); font-size: 16px; } +.step ul li::before { content: ""; width: 10px; height: 10px; margin-top: 8px; flex-shrink: 0; background: var(--accent); } +.step .hint { color: var(--dim); font-size: 14px; } +.step kbd { + font-family: var(--font-mono); font-size: 18px; line-height: 1; + background: var(--bg-2); color: var(--text); + padding: 4px 8px; box-shadow: inset 1px 1px 0 var(--bevel-lo), inset -1px -1px 0 var(--bevel-hi); +} + +/* ============================================================ + FOOTER + ============================================================ */ +.footer { border-top: 1px solid var(--line); background: var(--bg-2); } +.footer-in { display: flex; flex-wrap: wrap; gap: 32px; align-items: center; justify-content: space-between; padding-block: 40px; } +.footer .brand .name { font-size: 20px; } +.footer-links { display: flex; gap: 8px; flex-wrap: wrap; } +.footer .disc { color: var(--dim); font-size: 13px; max-width: 46ch; } + +/* ---- floating dust ---- */ +.dust { position: absolute; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; } +@keyframes rise { + 0% { transform: translateY(0) translateX(0); opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { transform: translateY(-110vh) translateX(24px); opacity: 0; } +} +@media (prefers-reduced-motion: reduce) { .dust { display: none; } } +.hero-grid { z-index: 1; } + +/* ---- head-font: Press Start 2P is wide/tall — scale down ---- */ +:root[data-head="8bit"] .section-title { font-size: clamp(20px, 3vw, 38px); line-height: 1.25; } +:root[data-head="8bit"] .hero-tagline { font-size: clamp(15px, 2vw, 26px); line-height: 1.4; } +:root[data-head="8bit"] .brand .name { font-size: 15px; } +:root[data-head="8bit"] .mc-btn { font-size: 12px; } +:root[data-head="8bit"] .mc-btn.sm { font-size: 11px; } +:root[data-head="8bit"] .step h3 { font-size: 17px; line-height: 1.35; } +:root[data-head="8bit"] .step .num { font-size: 30px; } +:root[data-head="8bit"] .feat h3 { font-size: 15px; line-height: 1.4; } +:root[data-head="8bit"] .serverlist .title { font-size: 16px; } +:root[data-head="8bit"] .tile .k { font-size: 26px; } +:root[data-head="8bit"] .ip-chip button { font-size: 12px; } +:root[data-head="8bit"] .live-pill { font-size: 12px; } + +/* ---- head-font: Silkscreen is small-cap-ish — nudge ---- */ +:root[data-head="silkscreen"] .section-title { letter-spacing: 0; } + +/* ============================================================ + SCROLL REVEAL (fail-safe: visible at rest; animates only when JS adds .anim) + ============================================================ */ +.reveal { will-change: opacity, transform; } +@keyframes reveal-in { + from { opacity: 0; transform: translateY(22px); } + to { opacity: 1; transform: none; } +} +@media (prefers-reduced-motion: no-preference) { + .reveal.anim { animation: reveal-in .6s cubic-bezier(.2,.7,.3,1) both; } +} + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 880px) { + :root[data-hero="split"] .hero-grid { grid-template-columns: 1fr; } + :root[data-hero="split"] .hero-status { display: block; } + .stat-grid { grid-template-columns: repeat(2, 1fr); } + .feat-grid { grid-template-columns: 1fr; } + .nav-links { display: none; } + .serverlist { flex-wrap: wrap; } + .serverlist .stat { text-align: left; } +} +@media (max-width: 560px) { + .step { grid-template-columns: 1fr; gap: 18px; } + .step .num { width: 64px; height: 64px; font-size: 32px; } + .ip-chip .ip-val { font-size: 22px; } + .stat-grid { grid-template-columns: 1fr 1fr; } +} diff --git a/landing/design/tweaks-panel.jsx b/landing/design/tweaks-panel.jsx new file mode 100644 index 0000000..bec00c3 --- /dev/null +++ b/landing/design/tweaks-panel.jsx @@ -0,0 +1,541 @@ +// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design) + +/* BEGIN USAGE */ +// tweaks-panel.jsx +// Reusable Tweaks shell + form-control helpers. +// Exports (to window): useTweaks, TweaksPanel, TweakSection, TweakRow, TweakSlider, +// TweakToggle, TweakRadio, TweakSelect, TweakText, TweakNumber, TweakColor, TweakButton. +// +// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode, +// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so +// individual prototypes don't re-roll it. Ships a consistent set of controls so you +// don't hand-draw , segmented radios, steppers, etc. +// +// Usage (in an HTML file that loads React + Babel): +// +// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ +// "primaryColor": "#D97757", +// "palette": ["#D97757", "#29261b", "#f6f4ef"], +// "fontSize": 16, +// "density": "regular", +// "dark": false +// }/*EDITMODE-END*/; +// +// function App() { +// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); +// return ( +//
+// Hello +// +// +// setTweak('fontSize', v)} /> +// setTweak('density', v)} /> +// +// setTweak('primaryColor', v)} /> +// setTweak('palette', v)} /> +// setTweak('dark', v)} /> +// +//
+// ); +// } +// +// TweakRadio is the segmented control for 2–3 short options (auto-falls-back to +// TweakSelect past ~16/~10 chars per label); reach for TweakSelect directly when +// options are many or long. For color tweaks always curate 3-4 options rather than +// a free picker; an option can also be a whole 2–5 color palette (the stored value +// is the array). The Tweak* controls are a floor, not a ceiling — build custom +// controls inside the panel if a tweak calls for UI they don't cover. +/* END USAGE */ +// ───────────────────────────────────────────────────────────────────────────── + +const __TWEAKS_STYLE = ` + .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px; + max-height:calc(100vh - 32px);display:flex;flex-direction:column; + transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right; + background:rgba(250,249,247,.78);color:#29261b; + -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%); + border:.5px solid rgba(255,255,255,.6);border-radius:14px; + box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18); + font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden} + .twk-hd{display:flex;align-items:center;justify-content:space-between; + padding:10px 8px 10px 14px;cursor:move;user-select:none} + .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em} + .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55); + width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1} + .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b} + .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px; + overflow-y:auto;overflow-x:hidden;min-height:0; + scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent} + .twk-body::-webkit-scrollbar{width:8px} + .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px} + .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px; + border:2px solid transparent;background-clip:content-box} + .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25); + border:2px solid transparent;background-clip:content-box} + .twk-row{display:flex;flex-direction:column;gap:5px} + .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px} + .twk-lbl{display:flex;justify-content:space-between;align-items:baseline; + color:rgba(41,38,27,.72)} + .twk-lbl>span:first-child{font-weight:500} + .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums} + + .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase; + color:rgba(41,38,27,.45);padding:10px 0 0} + .twk-sect:first-child{padding-top:0} + + .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px; + border:.5px solid rgba(0,0,0,.1);border-radius:7px; + background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none} + .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)} + select.twk-field{padding-right:22px; + background-image:url("data:image/svg+xml;utf8,"); + background-repeat:no-repeat;background-position:right 8px center} + + .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0; + border-radius:999px;background:rgba(0,0,0,.12);outline:none} + .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none; + width:14px;height:14px;border-radius:50%;background:#fff; + border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} + .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%; + background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} + + .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px; + background:rgba(0,0,0,.06);user-select:none} + .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px; + background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12); + transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s} + .twk-seg.dragging .twk-seg-thumb{transition:none} + .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0; + background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px; + border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2; + overflow-wrap:anywhere} + + .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px; + background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0} + .twk-toggle[data-on="1"]{background:#34c759} + .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%; + background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s} + .twk-toggle[data-on="1"] i{transform:translateX(14px)} + + .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px; + border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)} + .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize; + user-select:none;padding-right:8px} + .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent; + font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0; + outline:none;color:inherit;-moz-appearance:textfield} + .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{ + -webkit-appearance:none;margin:0} + .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)} + + .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px; + background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default} + .twk-btn:hover{background:rgba(0,0,0,.88)} + .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit} + .twk-btn.secondary:hover{background:rgba(0,0,0,.1)} + + .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px; + border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default; + background:transparent;flex-shrink:0} + .twk-swatch::-webkit-color-swatch-wrapper{padding:0} + .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px} + .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px} + + .twk-chips{display:flex;gap:6px} + .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px; + padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default; + box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06); + transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s} + .twk-chip:hover{transform:translateY(-1px); + box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)} + .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85), + 0 2px 6px rgba(0,0,0,.15)} + .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%; + display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)} + .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)} + .twk-chip>span>i:first-child{box-shadow:none} + .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px; + filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))} +`; + +// ── useTweaks ─────────────────────────────────────────────────────────────── +// Single source of truth for tweak values. setTweak persists via the host +// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk). +function useTweaks(defaults) { + const [values, setValues] = React.useState(defaults); + // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a + // useState-style call doesn't write a "[object Object]" key into the persisted + // JSON block. + const setTweak = React.useCallback((keyOrEdits, val) => { + const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null + ? keyOrEdits : { [keyOrEdits]: val }; + setValues((prev) => ({ ...prev, ...edits })); + window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); + // Same-window signal so in-page listeners (deck-stage rail thumbnails) + // can react — the parent message only reaches the host, not peers. + window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits })); + }, []); + return [values, setTweak]; +} + +// ── TweaksPanel ───────────────────────────────────────────────────────────── +// Floating shell. Registers the protocol listener BEFORE announcing +// availability — if the announce ran first, the host's activate could land +// before our handler exists and the toolbar toggle would silently no-op. +// The close button posts __edit_mode_dismissed so the host's toolbar toggle +// flips off in lockstep; the host echoes __deactivate_edit_mode back which +// is what actually hides the panel. +function TweaksPanel({ title = 'Tweaks', children }) { + const [open, setOpen] = React.useState(false); + const dragRef = React.useRef(null); + const offsetRef = React.useRef({ x: 16, y: 16 }); + const PAD = 16; + + const clampToViewport = React.useCallback(() => { + const panel = dragRef.current; + if (!panel) return; + const w = panel.offsetWidth, h = panel.offsetHeight; + const maxRight = Math.max(PAD, window.innerWidth - w - PAD); + const maxBottom = Math.max(PAD, window.innerHeight - h - PAD); + offsetRef.current = { + x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)), + y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)), + }; + panel.style.right = offsetRef.current.x + 'px'; + panel.style.bottom = offsetRef.current.y + 'px'; + }, []); + + React.useEffect(() => { + if (!open) return; + clampToViewport(); + if (typeof ResizeObserver === 'undefined') { + window.addEventListener('resize', clampToViewport); + return () => window.removeEventListener('resize', clampToViewport); + } + const ro = new ResizeObserver(clampToViewport); + ro.observe(document.documentElement); + return () => ro.disconnect(); + }, [open, clampToViewport]); + + React.useEffect(() => { + const onMsg = (e) => { + const t = e?.data?.type; + if (t === '__activate_edit_mode') setOpen(true); + else if (t === '__deactivate_edit_mode') setOpen(false); + }; + window.addEventListener('message', onMsg); + window.parent.postMessage({ type: '__edit_mode_available' }, '*'); + return () => window.removeEventListener('message', onMsg); + }, []); + + const dismiss = () => { + setOpen(false); + window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); + }; + + const onDragStart = (e) => { + const panel = dragRef.current; + if (!panel) return; + const r = panel.getBoundingClientRect(); + const sx = e.clientX, sy = e.clientY; + const startRight = window.innerWidth - r.right; + const startBottom = window.innerHeight - r.bottom; + const move = (ev) => { + offsetRef.current = { + x: startRight - (ev.clientX - sx), + y: startBottom - (ev.clientY - sy), + }; + clampToViewport(); + }; + const up = () => { + window.removeEventListener('mousemove', move); + window.removeEventListener('mouseup', up); + }; + window.addEventListener('mousemove', move); + window.addEventListener('mouseup', up); + }; + + if (!open) return null; + return ( + <> + +
+
+ {title} + +
+
+ {children} +
+
+ + ); +} + +// ── Layout helpers ────────────────────────────────────────────────────────── + +function TweakSection({ label, children }) { + return ( + <> +
{label}
+ {children} + + ); +} + +function TweakRow({ label, value, children, inline = false }) { + return ( +
+
+ {label} + {value != null && {value}} +
+ {children} +
+ ); +} + +// ── Controls ──────────────────────────────────────────────────────────────── + +function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { + return ( + + onChange(Number(e.target.value))} /> + + ); +} + +function TweakToggle({ label, value, onChange }) { + return ( +
+
{label}
+ +
+ ); +} + +function TweakRadio({ label, value, options, onChange }) { + const trackRef = React.useRef(null); + const [dragging, setDragging] = React.useState(false); + // The active value is read by pointer-move handlers attached for the lifetime + // of a drag — ref it so a stale closure doesn't fire onChange for every move. + const valueRef = React.useRef(value); + valueRef.current = value; + + // Segments wrap mid-word once per-segment width runs out. The track is + // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px + // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2 + // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall + // back to a dropdown rather than wrap. + const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length; + const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0); + const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0); + if (!fitsAsSegments) { + // onChange(e.target.value)}> + {options.map((o) => { + const v = typeof o === 'object' ? o.value : o; + const l = typeof o === 'object' ? o.label : o; + return ; + })} + + + ); +} + +function TweakText({ label, value, placeholder, onChange }) { + return ( + + onChange(e.target.value)} /> + + ); +} + +function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) { + const clamp = (n) => { + if (min != null && n < min) return min; + if (max != null && n > max) return max; + return n; + }; + const startRef = React.useRef({ x: 0, val: 0 }); + const onScrubStart = (e) => { + e.preventDefault(); + startRef.current = { x: e.clientX, val: value }; + const decimals = (String(step).split('.')[1] || '').length; + const move = (ev) => { + const dx = ev.clientX - startRef.current.x; + const raw = startRef.current.val + dx * step; + const snapped = Math.round(raw / step) * step; + onChange(clamp(Number(snapped.toFixed(decimals)))); + }; + const up = () => { + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + }; + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + }; + return ( +
+ {label} + onChange(clamp(Number(e.target.value)))} /> + {unit && {unit}} +
+ ); +} + +// Relative-luminance contrast pick — checkmarks drawn over a swatch need to +// read on both #111 and #fafafa without per-option configuration. Hex input +// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light". +function __twkIsLight(hex) { + const h = String(hex).replace('#', ''); + const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0'); + const n = parseInt(x.slice(0, 6), 16); + if (Number.isNaN(n)) return true; + const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + return r * 299 + g * 587 + b * 114 > 148000; +} + +const __TwkCheck = ({ light }) => ( + +); + +// TweakColor — curated color/palette picker. Each option is either a single +// hex string or an array of 1-5 hex strings; the card adapts — a lone color +// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the +// rest stacked in a sharp column on the right. onChange emits the +// option in the shape it was passed (string stays string, array stays array). +// Without options it falls back to the native color input for back-compat. +function TweakColor({ label, value, options, onChange }) { + if (!options || !options.length) { + return ( +
+
{label}
+ onChange(e.target.value)} /> +
+ ); + } + // Native emits lowercase hex per the HTML spec, so + // compare case-insensitively. String() guards JSON.stringify(undefined), + // which returns the primitive undefined (no .toLowerCase). + const key = (o) => String(JSON.stringify(o)).toLowerCase(); + const cur = key(value); + return ( + +
+ {options.map((o, i) => { + const colors = Array.isArray(o) ? o : [o]; + const [hero, ...rest] = colors; + const sup = rest.slice(0, 4); + const on = key(o) === cur; + return ( + + ); + })} +
+
+ ); +} + +function TweakButton({ label, onClick, secondary = false }) { + return ( + + ); +} + +Object.assign(window, { + useTweaks, TweaksPanel, TweakSection, TweakRow, + TweakSlider, TweakToggle, TweakRadio, TweakSelect, + TweakText, TweakNumber, TweakColor, TweakButton, +}); diff --git a/landing/design/uploads/logo-1780868316271.png b/landing/design/uploads/logo-1780868316271.png new file mode 100644 index 0000000..86dbead Binary files /dev/null and b/landing/design/uploads/logo-1780868316271.png differ diff --git a/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9Jn0Eiw.woff2 b/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9Jn0Eiw.woff2 new file mode 100644 index 0000000..fe867e9 Binary files /dev/null and b/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9Jn0Eiw.woff2 differ diff --git a/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnMEi1lR.woff2 b/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnMEi1lR.woff2 new file mode 100644 index 0000000..1bcb86a Binary files /dev/null and b/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnMEi1lR.woff2 differ diff --git a/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnkEi1lR.woff2 b/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnkEi1lR.woff2 new file mode 100644 index 0000000..cc32cbf Binary files /dev/null and b/landing/public/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnkEi1lR.woff2 differ diff --git a/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2 b/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2 new file mode 100644 index 0000000..ce97d0f Binary files /dev/null and b/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2 differ diff --git a/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2 b/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2 new file mode 100644 index 0000000..db732c2 Binary files /dev/null and b/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2 differ diff --git a/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2 b/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2 new file mode 100644 index 0000000..0f3474e Binary files /dev/null and b/landing/public/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2 differ diff --git a/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nRivN04w.woff2 b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nRivN04w.woff2 new file mode 100644 index 0000000..3ba1443 Binary files /dev/null and b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nRivN04w.woff2 differ diff --git a/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2 b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2 new file mode 100644 index 0000000..947a979 Binary files /dev/null and b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2 differ diff --git a/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nWivN04w.woff2 b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nWivN04w.woff2 new file mode 100644 index 0000000..d3829f9 Binary files /dev/null and b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nWivN04w.woff2 differ diff --git a/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nYivN04w.woff2 b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nYivN04w.woff2 new file mode 100644 index 0000000..6dcf15a Binary files /dev/null and b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nYivN04w.woff2 differ diff --git a/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nbivN04w.woff2 b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nbivN04w.woff2 new file mode 100644 index 0000000..0919c5f Binary files /dev/null and b/landing/public/fonts/e3t4euO8T-267oIAQAu6jDQyK3nbivN04w.woff2 differ diff --git a/landing/public/fonts/fonts.css b/landing/public/fonts/fonts.css new file mode 100644 index 0000000..8aa44ed --- /dev/null +++ b/landing/public/fonts/fonts.css @@ -0,0 +1,270 @@ +/* cyrillic */ +@font-face { + font-family: 'Pixelify Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnkEi1lR.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* latin-ext */ +@font-face { + font-family: 'Pixelify Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnMEi1lR.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Pixelify Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(/fonts/CHylV-3HFUT7aC4iv1TxGDR9Jn0Eiw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic */ +@font-face { + font-family: 'Pixelify Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnkEi1lR.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* latin-ext */ +@font-face { + font-family: 'Pixelify Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/CHylV-3HFUT7aC4iv1TxGDR9JnMEi1lR.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Pixelify Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/CHylV-3HFUT7aC4iv1TxGDR9Jn0Eiw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Press Start 2P'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/e3t4euO8T-267oIAQAu6jDQyK3nYivN04w.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Press Start 2P'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/e3t4euO8T-267oIAQAu6jDQyK3nRivN04w.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Press Start 2P'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/e3t4euO8T-267oIAQAu6jDQyK3nWivN04w.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* latin-ext */ +@font-face { + font-family: 'Press Start 2P'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/e3t4euO8T-267oIAQAu6jDQyK3nbivN04w.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Press Start 2P'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* latin-ext */ +@font-face { + font-family: 'Silkscreen'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/m8JXjfVPf62XiF7kO-i9YL1la1OD.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Silkscreen'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/m8JXjfVPf62XiF7kO-i9YLNlaw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* latin-ext */ +@font-face { + font-family: 'Silkscreen'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmKi2Oud.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Silkscreen'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmyi2A.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'VT323'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/pxiKyp0ihIEF2isQFJXGdg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'VT323'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/pxiKyp0ihIEF2isRFJXGdg.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'VT323'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/pxiKyp0ihIEF2isfFJU.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/landing/public/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmKi2Oud.woff2 b/landing/public/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmKi2Oud.woff2 new file mode 100644 index 0000000..1fb8afe Binary files /dev/null and b/landing/public/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmKi2Oud.woff2 differ diff --git a/landing/public/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmyi2A.woff2 b/landing/public/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmyi2A.woff2 new file mode 100644 index 0000000..15bb84c Binary files /dev/null and b/landing/public/fonts/m8JUjfVPf62XiF7kO-i9aAhAfmyi2A.woff2 differ diff --git a/landing/public/fonts/m8JXjfVPf62XiF7kO-i9YL1la1OD.woff2 b/landing/public/fonts/m8JXjfVPf62XiF7kO-i9YL1la1OD.woff2 new file mode 100644 index 0000000..77cb2ed Binary files /dev/null and b/landing/public/fonts/m8JXjfVPf62XiF7kO-i9YL1la1OD.woff2 differ diff --git a/landing/public/fonts/m8JXjfVPf62XiF7kO-i9YLNlaw.woff2 b/landing/public/fonts/m8JXjfVPf62XiF7kO-i9YLNlaw.woff2 new file mode 100644 index 0000000..285a17a Binary files /dev/null and b/landing/public/fonts/m8JXjfVPf62XiF7kO-i9YLNlaw.woff2 differ diff --git a/landing/public/fonts/pxiKyp0ihIEF2isQFJXGdg.woff2 b/landing/public/fonts/pxiKyp0ihIEF2isQFJXGdg.woff2 new file mode 100644 index 0000000..d9f1698 Binary files /dev/null and b/landing/public/fonts/pxiKyp0ihIEF2isQFJXGdg.woff2 differ diff --git a/landing/public/fonts/pxiKyp0ihIEF2isRFJXGdg.woff2 b/landing/public/fonts/pxiKyp0ihIEF2isRFJXGdg.woff2 new file mode 100644 index 0000000..5383719 Binary files /dev/null and b/landing/public/fonts/pxiKyp0ihIEF2isRFJXGdg.woff2 differ diff --git a/landing/public/fonts/pxiKyp0ihIEF2isfFJU.woff2 b/landing/public/fonts/pxiKyp0ihIEF2isfFJU.woff2 new file mode 100644 index 0000000..fd760b5 Binary files /dev/null and b/landing/public/fonts/pxiKyp0ihIEF2isfFJU.woff2 differ diff --git a/landing/src/components/CopyChip.astro b/landing/src/components/CopyChip.astro new file mode 100644 index 0000000..10bc22e --- /dev/null +++ b/landing/src/components/CopyChip.astro @@ -0,0 +1,17 @@ +--- +// MC-GUI slot + copy button. variant "ip" = big gold mono (server address); +// "url" = smaller, wraps (auth / packwiz URLs). Copy handled by the global +// [data-copy] script in [...lang].astro; data-label / data-copied drive the +// (localized) button text and transient feedback. +interface Props { + value: string; + variant?: "ip" | "url"; + label?: string; + copiedLabel?: string; +} +const { value, variant = "ip", label = "Copy", copiedLabel = "Copied!" } = Astro.props; +--- +
+ {value} + +
diff --git a/landing/src/components/Creeper.astro b/landing/src/components/Creeper.astro new file mode 100644 index 0000000..dd2002d --- /dev/null +++ b/landing/src/components/Creeper.astro @@ -0,0 +1,17 @@ +--- +// 8x8 creeper face — pure markup, styled by .creeper in main.css. +const CREEPER = [ + "00000000", + "01100110", + "01100110", + "00011000", + "00111100", + "00111100", + "00100100", + "00000000", +]; +const cells = CREEPER.join("").split(""); +--- + diff --git a/landing/src/components/PixelIcon.astro b/landing/src/components/PixelIcon.astro new file mode 100644 index 0000000..37638a7 --- /dev/null +++ b/landing/src/components/PixelIcon.astro @@ -0,0 +1,20 @@ +--- +// 7x7 feature glyphs — styled by .picon in main.css. +interface Props { + glyph: "shield" | "diamond" | "orb" | "heart"; + gold?: boolean; +} +const { glyph, gold = false } = Astro.props; + +const GLYPHS: Record = { + shield: ["0111110", "1111111", "1111111", "1111111", "0111110", "0011100", "0001000"], + diamond: ["0001000", "0011100", "0111110", "1111111", "0111110", "0011100", "0001000"], + orb: ["0011100", "0111110", "1111111", "1111111", "1111111", "0111110", "0011100"], + heart: ["0110110", "1111111", "1111111", "1111111", "0111110", "0011100", "0001000"], +}; +const cells = GLYPHS[glyph].join("").split(""); +const onClass = gold ? "go" : "on"; +--- + diff --git a/landing/src/components/ServerListPanel.astro b/landing/src/components/ServerListPanel.astro new file mode 100644 index 0000000..7d5169b --- /dev/null +++ b/landing/src/components/ServerListPanel.astro @@ -0,0 +1,31 @@ +--- +// Static server-list panel — mirrors the multiplayer-list row. No live count +// (plan 09-landing.md option B); shows version, motd and slot cap. +import Creeper from "./Creeper.astro"; +import { site } from "../data/site"; +interface Props { + modded: string; + motd: string; + upTo: string; +} +const { modded, motd, upTo } = Astro.props; +const { status } = site; +--- +
+
+
+
+ {site.name} + Java {site.mcVersion} +
+

+ ⛏ {modded} · {motd} +

+
+
+
+ + {upTo} {status.slots} +
+
+
diff --git a/landing/src/data/site.ts b/landing/src/data/site.ts index f6ebe61..60abd19 100644 --- a/landing/src/data/site.ts +++ b/landing/src/data/site.ts @@ -1,11 +1,28 @@ -// Single source of truth for the landing page content. +// Single source of truth for the landing page's *non-translatable* config: +// domains, URLs, theme, launcher files, and the structural shape of the +// stat/feature lists. Translatable copy lives in src/i18n/ui.ts. // BASE_DOMAIN is read at build time; falls back to ulicraft.local. const BASE_DOMAIN = process.env.BASE_DOMAIN ?? "ulicraft.local"; +// Minecraft / pack facts, surfaced in a few places. +const MC_VERSION = "1.21.1"; + export const site = { name: "Ulicraft", - tagline: "LAN-party Minecraft server", baseDomain: BASE_DOMAIN, + mcVersion: MC_VERSION, + + // Design knobs (replace the design's in-browser TweaksPanel). Baked at build. + // mood: "grass" | "nether" | "end" + // hero: "centered" | "split" | "spotlight" + // headFont: "pixelify" | "8bit" | "silkscreen" + // dust: floating pixel particles in the hero + theme: { + mood: "grass", + hero: "centered", + headFont: "pixelify", + dust: true, + }, // Connect target shown to guests. No port needed — a dnsmasq SRV record // (_minecraft._tcp.mc.) points clients at 25565. @@ -21,10 +38,35 @@ export const site = { // Caddy local CA root — guests import this to trust the offline asset mirror. caCertUrl: `http://${BASE_DOMAIN}/ca.crt`, + // Static server-list panel (Status section). No fake live count — see plan + // 09-landing.md option B. + status: { + slots: 10, + }, + + // Honest stat tiles. Keys (numbers/versions) are language-neutral; labels + // come from ui.ts (same order). + stats: [ + { key: "50+" }, + { key: MC_VERSION }, + { key: "10" }, + { key: "6h" }, + ], + + // Feature cards. glyph ∈ shield|diamond|orb|heart; gold = gold pixel fill. + // Text (h/p) comes from ui.ts (same order). + features: [ + { glyph: "diamond", gold: true }, + { glyph: "shield", gold: false }, + { glyph: "orb", gold: false }, + { glyph: "heart", gold: true }, + ], + // Launcher downloads. Stable filenames are symlinks created by - // tooling/fetch-launcher.sh — keep these in sync with that script. + // tooling/fetch-launcher.sh — keep these in sync with that script. OS names + // and hints are technical, kept language-neutral here. launcher: { - name: "FjordLauncherUnlocked", + name: "Fjord Launcher", base: "/launcher/latest", downloads: [ { os: "Windows", file: "fjord-windows-setup.exe", hint: "x64 installer" }, @@ -33,3 +75,18 @@ export const site = { ], }, } as const; + +// Product / in-game UI literals — never translated (they match what guests see +// on screen or type verbatim). +export const LITERALS = { + authlib: "authlib-injector", + multiplayer: "Multiplayer", + addServer: "Add Server", +} as const; + +// Head-font CSS stacks, keyed by theme.headFont. +export const HEAD_FONTS: Record = { + pixelify: "'Pixelify Sans', system-ui, sans-serif", + "8bit": "'Press Start 2P', monospace", + silkscreen: "'Silkscreen', system-ui, sans-serif", +}; diff --git a/landing/src/i18n/ui.ts b/landing/src/i18n/ui.ts new file mode 100644 index 0000000..51f7454 --- /dev/null +++ b/landing/src/i18n/ui.ts @@ -0,0 +1,340 @@ +// Translatable UI copy for the landing page. Structure is identical across +// locales; the page (src/pages/[...lang].astro) renders against it. +// +// Conventions: +// - "{name}" in step1 is replaced with the launcher name at render time. +// - Sentences containing links/kbd/literals are split into parts (a/b/pre/ +// post/link) so the markup stays in the template, not in the strings. +// - Product / in-game literals (authlib-injector, Multiplayer, Add Server) +// live in site.ts LITERALS and are never translated. + +export type Lang = "en" | "es" | "eu"; +export const LANGS: Lang[] = ["en", "es", "eu"]; +export const LANG_LABEL: Record = { en: "EN", es: "ES", eu: "EU" }; +// URL path per locale (en is default, served at root). +export const LANG_PATH: Record = { en: "/", es: "/es/", eu: "/eu/" }; + +export interface UI { + htmlLang: string; + copied: string; // transient copy-button feedback + nav: { status: string; features: string; join: string; play: string }; + hero: { + eyebrow: string; // "Modded LAN · NeoForge" — version appended at render + tagline: string; + sub: string; + copyIp: string; + howToJoin: string; + metaModpack: string; + metaLan: string; + }; + status: { + eyebrow: string; + title: string; + lead: string; + modded: string; + motd: string; + upTo: string; + }; + statLabels: [string, string, string, string]; + features: { + eyebrow: string; + title: string; + lead: string; + items: { h: string; p: string }[]; + }; + join: { + eyebrow: string; + title: string; + lead: string; + s1: { kicker: string; h: string; p: string }; + s2: { + kicker: string; + h: string; + pa: string; // before the authlib-injector literal + pb: string; // after it + copy: string; + registerPre: string; + caPre: string; + caLink: string; + caPost: string; + }; + s3: { kicker: string; h: string; p: string }; + s4: { + kicker: string; + h: string; + pa: string; // before the Multiplayer/Add Server kbds + pb: string; // after them + copy: string; + }; + }; + footer: { join: string; disc: string }; +} + +export const ui: Record = { + en: { + htmlLang: "en", + copied: "Copied!", + nav: { status: "Status", features: "Features", join: "How to Join", play: "Play Now" }, + hero: { + eyebrow: "Modded LAN · NeoForge", + tagline: "Kitchen-sink survival, built to outlast the weekend.", + sub: + "A self-hosted modded server for the crew — a curated tech + magic + " + + "exploration pack, your own skins, one-click setup. Grab the address, " + + "import the pack, dig in.", + copyIp: "Copy IP", + howToJoin: "How to Join", + metaModpack: "NeoForge modpack", + metaLan: "LAN-only", + }, + status: { + eyebrow: "Server", + title: "This is what you'll see.", + lead: "Exactly the entry that shows up in your multiplayer list.", + modded: "Modded", + motd: "NeoForge modpack · Simple Voice Chat · LAN-only", + upTo: "up to", + }, + statLabels: ["Mods", "NeoForge", "Player slots", "World backups"], + features: { + eyebrow: "What makes it Ulicraft", + title: "Built for the crew.", + lead: + "Self-hosted, curated, and persistent — a modded server that respects " + + "your time and outlives the LAN.", + items: [ + { + h: "Kitchen-sink pack", + p: "~50–100 hand-picked mods — tech, magic, exploration and QoL. NeoForge 1.21.1, curated, no bloat.", + }, + { + h: "Your identity, self-hosted", + p: "Drasl runs accounts and skins on the LAN. No Mojang login needed — your skin and cape just work.", + }, + { + h: "One-click setup", + p: "packwiz installs and updates the whole modpack inside your launcher, so everyone stays in sync.", + }, + { + h: "Built to last", + p: "Not a weekend throwaway — it runs on the homelab with 6-hour world backups. Your base survives.", + }, + ], + }, + join: { + eyebrow: "How to Join · 4 Steps", + title: "From zero to spawning in.", + lead: "One-time setup. After this, the launcher keeps you in sync.", + s1: { + kicker: "Launcher", + h: "Download {name}", + p: "Grab {name} for your system — a Prism-based launcher with authlib-injector support built in.", + }, + s2: { + kicker: "Account", + h: "Add your account", + pa: "In the launcher, add an", + pb: "account using this URL, then log in with your Ulicraft credentials.", + copy: "Copy", + registerPre: "Need credentials? Register at", + caPre: "Offline party? Download and trust the", + caLink: "local CA certificate", + caPost: "so the game-file mirror works over HTTPS.", + }, + s3: { + kicker: "Modpack", + h: "Import the modpack", + p: "Add a new instance from this packwiz URL — it installs and updates the whole pack.", + }, + s4: { + kicker: "Connect", + h: "Join the server", + pa: "Launch the instance, open", + pb: ", paste the address, and join.", + copy: "Copy IP", + }, + }, + footer: { + join: "How to Join", + disc: "Not affiliated with Mojang or Microsoft. Minecraft is a trademark of Mojang AB.", + }, + }, + + es: { + htmlLang: "es", + copied: "¡Copiado!", + nav: { status: "Estado", features: "Características", join: "Cómo entrar", play: "Jugar ahora" }, + hero: { + eyebrow: "LAN con mods · NeoForge", + tagline: "Supervivencia todo incluido, para durar más que el finde.", + sub: + "Un servidor con mods autoalojado para la cuadrilla: un pack curado de " + + "tecnología, magia y exploración, tus propias skins y configuración en un " + + "clic. Copia la dirección, importa el pack y a cavar.", + copyIp: "Copiar IP", + howToJoin: "Cómo entrar", + metaModpack: "Pack NeoForge", + metaLan: "Solo LAN", + }, + status: { + eyebrow: "Servidor", + title: "Esto es lo que verás.", + lead: "La misma entrada que aparece en tu lista de multijugador.", + modded: "Con mods", + motd: "Pack NeoForge · Simple Voice Chat · Solo LAN", + upTo: "hasta", + }, + statLabels: ["Mods", "NeoForge", "Plazas", "Copias del mundo"], + features: { + eyebrow: "Qué hace especial a Ulicraft", + title: "Hecho para la cuadrilla.", + lead: + "Autoalojado, curado y persistente: un servidor con mods que respeta tu " + + "tiempo y sobrevive a la LAN.", + items: [ + { + h: "Pack todo incluido", + p: "~50–100 mods elegidos a mano: tecnología, magia, exploración y QoL. NeoForge 1.21.1, curado y sin relleno.", + }, + { + h: "Tu identidad, autoalojada", + p: "Drasl gestiona cuentas y skins en la LAN. Sin login de Mojang: tu skin y tu capa funcionan sin más.", + }, + { + h: "Configuración en un clic", + p: "packwiz instala y actualiza todo el modpack dentro de tu launcher, así todos vais sincronizados.", + }, + { + h: "Hecho para durar", + p: "No es de usar y tirar: corre en el homelab con copias del mundo cada 6 horas. Tu base sobrevive.", + }, + ], + }, + join: { + eyebrow: "Cómo entrar · 4 pasos", + title: "De cero a aparecer en el mundo.", + lead: "Configuración única. Después, el launcher te mantiene sincronizado.", + s1: { + kicker: "Launcher", + h: "Descarga {name}", + p: "Coge {name} para tu sistema: un launcher basado en Prism con soporte authlib-injector integrado.", + }, + s2: { + kicker: "Cuenta", + h: "Añade tu cuenta", + pa: "En el launcher, añade una cuenta", + pb: "con esta URL y luego inicia sesión con tus credenciales de Ulicraft.", + copy: "Copiar", + registerPre: "¿Sin credenciales? Regístrate en", + caPre: "¿Fiesta sin internet? Descarga y confía en el", + caLink: "certificado CA local", + caPost: "para que el mirror de archivos funcione por HTTPS.", + }, + s3: { + kicker: "Modpack", + h: "Importa el modpack", + p: "Añade una instancia nueva desde esta URL de packwiz: instala y actualiza todo el pack.", + }, + s4: { + kicker: "Conectar", + h: "Entra al servidor", + pa: "Lanza la instancia, abre", + pb: ", pega la dirección y entra.", + copy: "Copiar IP", + }, + }, + footer: { + join: "Cómo entrar", + disc: "No afiliado a Mojang ni Microsoft. Minecraft es una marca de Mojang AB.", + }, + }, + + eu: { + htmlLang: "eu", + copied: "Kopiatuta!", + nav: { status: "Egoera", features: "Ezaugarriak", join: "Nola sartu", play: "Jokatu orain" }, + hero: { + eyebrow: "Mod-dun LANa · NeoForge", + tagline: "Mod ugariko biziraupena, irauteko egina.", + sub: + "Koadrilarentzako mod-dun zerbitzari auto-ostatatua: teknologia, magia eta " + + "esplorazio pack zaindua, zure azalak eta konfigurazioa klik bakarrean. " + + "Hartu helbidea, inportatu packa eta hasi zulatzen.", + copyIp: "Kopiatu IPa", + howToJoin: "Nola sartu", + metaModpack: "NeoForge packa", + metaLan: "LAN soilik", + }, + status: { + eyebrow: "Zerbitzaria", + title: "Hau ikusiko duzu.", + lead: "Zure multijokalari-zerrendan agertzen den sarrera bera.", + modded: "Mod-duna", + motd: "NeoForge packa · Simple Voice Chat · LAN soilik", + upTo: "gehienez", + }, + statLabels: ["Modak", "NeoForge", "Plazak", "Munduaren babeskopiak"], + features: { + eyebrow: "Zerk egiten du Ulicraft berezi", + title: "Koadrilarentzat eginda.", + lead: + "Auto-ostatatua, zaindua eta iraunkorra: zure denbora errespetatzen duen " + + "eta LANa gainditzen duen mod-dun zerbitzaria.", + items: [ + { + h: "Pack osoa", + p: "Eskuz aukeratutako ~50–100 mod: teknologia, magia, esplorazioa eta QoL. NeoForge 1.21.1, zaindua eta betegarririk gabe.", + }, + { + h: "Zure nortasuna, auto-ostatatua", + p: "Draslek kontuak eta azalak LANean kudeatzen ditu. Mojang loginik gabe: zure azalak eta kapak besterik gabe dabiltza.", + }, + { + h: "Konfigurazioa klik batean", + p: "packwiz-ek modpack osoa instalatu eta eguneratzen du zure launcherrean, denok sinkronizatuta egoteko.", + }, + { + h: "Irauteko eginda", + p: "Ez da behin erabili eta botatzekoa: homelab-ean dabil, munduaren babeskopiak 6 orduro. Zure basea bizirik.", + }, + ], + }, + join: { + eyebrow: "Nola sartu · 4 urrats", + title: "Zerotik mundura agertzera.", + lead: "Behin bakarrik konfiguratu. Gero, launcherrak sinkronizatuta mantenduko zaitu.", + s1: { + kicker: "Launcherra", + h: "Deskargatu {name}", + p: "Hartu {name} zure sistemarako: Prism-en oinarritutako launcherra, authlib-injector euskarriarekin.", + }, + s2: { + kicker: "Kontua", + h: "Gehitu zure kontua", + pa: "Launcherrean, gehitu", + pb: "kontu bat URL honekin, eta gero hasi saioa zure Ulicraft kredentzialekin.", + copy: "Kopiatu", + registerPre: "Kredentzialik ez? Erregistratu hemen:", + caPre: "Internetik gabeko festa? Deskargatu eta fidatu", + caLink: "tokiko CA ziurtagiriaz", + caPost: "fitxategi-mirrorrak HTTPS bidez ibil dadin.", + }, + s3: { + kicker: "Modpacka", + h: "Inportatu modpacka", + p: "Gehitu instantzia berri bat packwiz URL honetatik: pack osoa instalatu eta eguneratzen du.", + }, + s4: { + kicker: "Konektatu", + h: "Sartu zerbitzarira", + pa: "Abiarazi instantzia, ireki", + pb: ", itsatsi helbidea eta sartu.", + copy: "Kopiatu IPa", + }, + }, + footer: { + join: "Nola sartu", + disc: "Ez dago Mojang edo Microsoft-ekin lotuta. Minecraft Mojang AB-ren marka da.", + }, + }, +}; diff --git a/landing/src/pages/[...lang].astro b/landing/src/pages/[...lang].astro new file mode 100644 index 0000000..c8ef2de --- /dev/null +++ b/landing/src/pages/[...lang].astro @@ -0,0 +1,317 @@ +--- +import { site, LITERALS, HEAD_FONTS } from "../data/site"; +import { ui, LANGS, LANG_LABEL, LANG_PATH, type Lang } from "../i18n/ui"; +import Creeper from "../components/Creeper.astro"; +import PixelIcon from "../components/PixelIcon.astro"; +import CopyChip from "../components/CopyChip.astro"; +import ServerListPanel from "../components/ServerListPanel.astro"; +import "../styles/main.css"; + +// One static page per locale: en at "/", es at "/es/", eu at "/eu/". +export function getStaticPaths() { + return [ + { params: { lang: undefined }, props: { locale: "en" as Lang } }, + { params: { lang: "es" }, props: { locale: "es" as Lang } }, + { params: { lang: "eu" }, props: { locale: "eu" as Lang } }, + ]; +} + +const { locale } = Astro.props; +const t = ui[locale]; +const { theme, launcher } = site; +const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify; +const launcherName = launcher.name; + +// Build-time floating dust bits (index-derived, no runtime RNG). +const dustBits = Array.from({ length: 16 }, (_, i) => { + const size = 3 + (i % 3) * 2; + return { + left: `${(i * 61) % 100}%`, + size: `${size}px`, + bg: i % 4 === 0 ? "var(--gold)" : "var(--accent)", + opacity: 0.18 + (i % 3) * 0.06, + anim: `rise ${13 + (i % 7) * 3}s linear ${i * 0.7}s infinite`, + }; +}); +--- + + + + + + + {site.name} — {t.hero.howToJoin} + + + + {LANGS.map((l) => )} + + + + + +
+ +
+ {theme.dust && ( + + )} +
+ +
+ {t.hero.eyebrow} {site.mcVersion} +

{t.hero.tagline}

+

{t.hero.sub}

+ +
+ Java {site.mcVersion} + + {t.hero.metaModpack} + + {t.hero.metaLan} +
+
+
+ +
+
+
+ + +
+
+
+ {t.status.eyebrow} +

{t.status.title}

+

{t.status.lead}

+
+
+ +
+
+ {site.stats.map((s, i) => ( +
+
{s.key}
+
{t.statLabels[i]}
+
+ ))} +
+
+
+ + +
+
+
+ {t.features.eyebrow} +

{t.features.title}

+

{t.features.lead}

+
+
+ {site.features.map((f, i) => ( +
+ +
+

{t.features.items[i].h}

+

{t.features.items[i].p}

+
+
+ ))} +
+
+
+ + +
+
+
+ {t.join.eyebrow} +

{t.join.title}

+

{t.join.lead}

+
+ +
+ +
+
1
+
+ {t.join.s1.kicker} +

{t.join.s1.h.replace("{name}", launcherName)}

+

{t.join.s1.p.replace(/\{name\}/g, launcherName)}

+
+ {launcher.downloads.map((d) => ( + + {d.os} · {d.hint} + + ))} +
+
+
+ + +
+
2
+
+ {t.join.s2.kicker} +

{t.join.s2.h}

+

+ {t.join.s2.pa} {LITERALS.authlib} {t.join.s2.pb} +

+
+ +
+

+ {t.join.s2.registerPre} {site.authUrl}. +

+ + {t.join.s2.caPre} {t.join.s2.caLink} {t.join.s2.caPost} + +
+
+ + +
+
3
+
+ {t.join.s3.kicker} +

{t.join.s3.h}

+

{t.join.s3.p}

+
+ +
+
+
+ + +
+
4
+
+ {t.join.s4.kicker} +

{t.join.s4.h}

+

+ {t.join.s4.pa} {LITERALS.multiplayer}{LITERALS.addServer}{t.join.s4.pb} +

+
+ +
+
+
+
+
+
+
+ + + + + + + diff --git a/landing/src/pages/index.astro b/landing/src/pages/index.astro deleted file mode 100644 index 56d7105..0000000 --- a/landing/src/pages/index.astro +++ /dev/null @@ -1,199 +0,0 @@ ---- -import { site } from "../data/site"; -const { launcher } = site; ---- - - - - - - - {site.name} — join the server - - - - -
-
- -

{site.name}

-

{site.tagline}

-

- Server - {site.serverAddress} - -

-
- -
    -
  1. -

    1 Download the launcher

    -

    Grab {launcher.name} for your system.

    -
    - {launcher.downloads.map((d) => ( - - {d.os} - {d.hint} - - ))} -
    -
  2. - -
  3. -

    2 Add your account

    -

    In the launcher, add an authlib-injector account using this URL:

    -

    - {site.authlibUrl} - -

    -

    - Need credentials? Register at {site.authUrl}, then log in - with that username and password. -

    -

    - Offline party? Download and trust the - local CA certificate so the game-file - mirror works over HTTPS. -

    -
  4. - -
  5. -

    3 Import the modpack

    -

    Add a new instance from this packwiz URL:

    -

    - {site.packwizUrl} - -

    -
  6. - -
  7. -

    4 Connect

    -

    Launch the instance, add a server, and join:

    -

    - {site.serverAddress} - -

    -
  8. -
- -
-

{site.name} · {site.baseDomain}

-
-
- - - - - - diff --git a/landing/src/styles/main.css b/landing/src/styles/main.css new file mode 100644 index 0000000..6033c4e --- /dev/null +++ b/landing/src/styles/main.css @@ -0,0 +1,608 @@ +/* ============================================================ + ULICRAFT — "Carved from stone" + Design system: dark overworld, MC-GUI bevels, pixel type + ============================================================ */ + +/* ---- Mood palettes (data-mood on ) ---- */ +:root, +:root[data-mood="grass"] { + --bg: oklch(0.165 0.014 150); + --bg-2: oklch(0.205 0.016 150); + --surface: oklch(0.235 0.017 150); + --surface-2: oklch(0.285 0.018 150); + --slot: oklch(0.185 0.014 150); + --bevel-hi: oklch(0.40 0.018 150); + --bevel-lo: oklch(0.115 0.012 150); + --line: oklch(0.33 0.016 150); + + --accent: oklch(0.72 0.16 145); /* grass */ + --accent-hi: oklch(0.80 0.15 145); + --accent-lo: oklch(0.55 0.15 145); + --accent-ink: oklch(0.17 0.05 150); /* text on accent */ + + --gold: oklch(0.82 0.135 85); + --glow: oklch(0.72 0.16 145 / 0.30); + + --text: oklch(0.95 0.008 110); + --muted: oklch(0.72 0.012 145); + --dim: oklch(0.55 0.012 145); +} + +:root[data-mood="nether"] { + --bg: oklch(0.165 0.018 35); + --bg-2: oklch(0.205 0.022 32); + --surface: oklch(0.235 0.026 32); + --surface-2: oklch(0.285 0.030 32); + --slot: oklch(0.185 0.020 32); + --bevel-hi: oklch(0.42 0.040 35); + --bevel-lo: oklch(0.115 0.016 32); + --line: oklch(0.34 0.030 32); + + --accent: oklch(0.64 0.19 32); /* nether red */ + --accent-hi: oklch(0.72 0.18 38); + --accent-lo: oklch(0.50 0.17 30); + --accent-ink: oklch(0.16 0.04 32); + + --gold: oklch(0.83 0.14 75); + --glow: oklch(0.64 0.19 32 / 0.34); + + --text: oklch(0.95 0.010 60); + --muted: oklch(0.74 0.020 45); + --dim: oklch(0.56 0.020 40); +} + +:root[data-mood="end"] { + --bg: oklch(0.155 0.018 305); + --bg-2: oklch(0.195 0.022 305); + --surface: oklch(0.225 0.026 305); + --surface-2: oklch(0.275 0.030 305); + --slot: oklch(0.175 0.020 305); + --bevel-hi: oklch(0.42 0.040 305); + --bevel-lo: oklch(0.110 0.016 305); + --line: oklch(0.34 0.030 305); + + --accent: oklch(0.74 0.135 175); /* end teal */ + --accent-hi: oklch(0.82 0.13 175); + --accent-lo: oklch(0.58 0.13 178); + --accent-ink: oklch(0.16 0.04 200); + + --gold: oklch(0.84 0.13 95); + --glow: oklch(0.70 0.16 300 / 0.34); + + --text: oklch(0.96 0.010 300); + --muted: oklch(0.76 0.020 300); + --dim: oklch(0.58 0.020 300); +} + +/* ---- Fonts (switchable head face via --font-head) ---- */ +:root { + --font-head: 'Pixelify Sans', system-ui, sans-serif; + --font-body: 'Space Grotesk', system-ui, sans-serif; + --font-pixel: 'Press Start 2P', monospace; /* tiny eyebrow labels */ + --font-mono: 'VT323', monospace; /* IP / terminal */ + --maxw: 1160px; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-body); + font-size: 17px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +/* faint pixel-grid stone texture over everything */ +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: + repeating-linear-gradient(0deg, oklch(1 0 0 / 0.018) 0 1px, transparent 1px 4px), + repeating-linear-gradient(90deg, oklch(0 0 0 / 0.06) 0 1px, transparent 1px 4px); + background-size: 4px 4px, 4px 4px; + mix-blend-mode: overlay; + opacity: 0.5; +} + +img { display: block; max-width: 100%; } + +::selection { background: var(--accent); color: var(--accent-ink); } + +/* ---- Typography helpers ---- */ +.eyebrow { + font-family: var(--font-pixel); + font-size: clamp(9px, 1.1vw, 11px); + letter-spacing: 0.12em; + color: var(--accent); + text-transform: uppercase; + line-height: 1.8; +} +h1, h2, h3 { + font-family: var(--font-head); + font-weight: 600; + line-height: 1.02; + letter-spacing: 0.01em; + margin: 0; +} +.section-title { + font-size: clamp(34px, 5vw, 58px); +} +.lead { color: var(--muted); } +.mono { font-family: var(--font-mono); } + +/* in-copy links (steps, hints) — accent, not browser blue */ +.step a, .hint a, .hero-sub a { color: var(--accent); text-underline-offset: 2px; } + +/* ---- Layout ---- */ +.wrap { width: min(var(--maxw), calc(100% - 48px)); margin-inline: auto; } +section { position: relative; z-index: 1; } +.pad { padding-block: clamp(64px, 9vw, 130px); } +.sec-head { max-width: 640px; margin-bottom: 48px; } +.sec-head .eyebrow { display: block; margin-bottom: 14px; } +.sec-head p { margin: 16px 0 0; font-size: 18px; } + +/* ============================================================ + MINECRAFT-STYLE BUTTON + ============================================================ */ +.mc-btn { + --b: var(--surface-2); + position: relative; + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--font-head); + font-size: 17px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--text); + background: var(--b); + border: 0; + padding: 15px 24px 17px; + cursor: pointer; + text-decoration: none; + box-shadow: + inset 2px 2px 0 var(--bevel-hi), + inset -2px -2px 0 var(--bevel-lo), + 0 4px 0 oklch(0 0 0 / 0.5), + 0 8px 18px oklch(0 0 0 / 0.4); + transition: transform .08s ease, filter .12s ease; + image-rendering: pixelated; + text-shadow: 0 2px 0 oklch(0 0 0 / 0.35); + white-space: nowrap; +} +.mc-btn:hover { filter: brightness(1.12); } +.mc-btn:active { + transform: translateY(4px); + box-shadow: + inset 2px 2px 0 var(--bevel-hi), + inset -2px -2px 0 var(--bevel-lo), + 0 0 0 oklch(0 0 0 / 0.5), + 0 2px 8px oklch(0 0 0 / 0.4); +} +.mc-btn.primary { + --b: var(--accent); + --bevel-hi: var(--accent-hi); + --bevel-lo: var(--accent-lo); + color: var(--accent-ink); + text-shadow: 0 2px 0 oklch(1 0 0 / 0.22); +} +.mc-btn.gold { + --b: var(--gold); + --bevel-hi: oklch(0.90 0.10 90); + --bevel-lo: oklch(0.66 0.13 75); + color: oklch(0.20 0.05 80); + text-shadow: 0 2px 0 oklch(1 0 0 / 0.25); +} +.mc-btn.sm { font-size: 14px; padding: 10px 16px 12px; } + +/* ---- Copy-IP chip ---- */ +.ip-chip { + display: inline-flex; + align-items: stretch; + background: var(--slot); + box-shadow: + inset 2px 2px 0 var(--bevel-lo), + inset -2px -2px 0 var(--bevel-hi); + overflow: hidden; +} +.ip-chip .ip-val { + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px; + font-family: var(--font-mono); + font-size: 28px; + line-height: 1; + color: var(--gold); + letter-spacing: 0.02em; +} +.ip-chip .ip-val .pin { color: var(--dim); font-size: 22px; } +.ip-chip button { + border: 0; + cursor: pointer; + padding: 14px 18px; + font-family: var(--font-head); + font-weight: 600; + font-size: 15px; + background: var(--accent); + color: var(--accent-ink); + box-shadow: inset 2px 2px 0 var(--accent-hi), inset -2px -2px 0 var(--accent-lo); + transition: filter .12s ease; + white-space: nowrap; +} +.ip-chip button:hover { filter: brightness(1.1); } +.ip-chip button.copied { background: var(--gold); color: oklch(0.20 0.05 80); } + +/* url variant: long auth/packwiz URLs — smaller, wraps instead of overflowing */ +.ip-chip.url { max-width: 100%; } +.ip-chip.url .ip-val { + font-family: var(--font-mono); + font-size: 18px; + padding: 8px 14px; + word-break: break-all; + color: var(--text); +} +.ip-chip.url button { font-size: 14px; padding: 10px 16px; } + +/* ============================================================ + NAV + ============================================================ */ +.nav { + position: sticky; + top: 0; + z-index: 50; + background: oklch(0.165 0.014 150 / 0.72); + -webkit-backdrop-filter: blur(14px) saturate(140%); + backdrop-filter: blur(14px) saturate(140%); + border-bottom: 1px solid var(--line); +} +.nav-in { + display: flex; + align-items: center; + gap: 24px; + height: 68px; +} +.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); } +.brand .name { + font-family: var(--font-head); + font-weight: 600; + font-size: 22px; + letter-spacing: 0.02em; +} +.nav-links { display: flex; gap: 6px; margin-left: 12px; } + +/* language switcher */ +.lang-switch { display: flex; gap: 2px; } +.lang-switch a { + font-family: var(--font-pixel); + font-size: 10px; + letter-spacing: 0.06em; + color: var(--dim); + text-decoration: none; + padding: 7px 8px; + line-height: 1; + transition: color .12s ease, background .12s ease; +} +.lang-switch a:hover { color: var(--text); background: var(--surface); } +.lang-switch a.on { color: var(--accent-ink); background: var(--accent); } +.nav-links a { + color: var(--muted); + text-decoration: none; + font-size: 15px; + font-weight: 500; + padding: 8px 12px; + transition: color .12s ease, background .12s ease; +} +.nav-links a:hover { color: var(--text); background: var(--surface); } +.nav-spacer { flex: 1; } + +/* ---- Creeper pixel mark ---- */ +.creeper { + display: grid; + grid-template-columns: repeat(8, 1fr); + grid-template-rows: repeat(8, 1fr); + width: 34px; + height: 34px; + background: var(--accent); + box-shadow: inset 2px 2px 0 var(--accent-hi), inset -2px -2px 0 var(--accent-lo); + image-rendering: pixelated; +} +.creeper i { background: transparent; } +.creeper i.f { background: oklch(0.16 0.03 150); } + +/* ============================================================ + HERO + ============================================================ */ +.hero { + position: relative; + padding-top: clamp(48px, 7vw, 90px); + padding-bottom: clamp(56px, 8vw, 110px); + overflow: hidden; +} +.hero::before { /* spotlight glow behind logo */ + content: ""; + position: absolute; + top: -10%; + left: 50%; + width: 1100px; + height: 760px; + transform: translateX(-50%); + background: radial-gradient(closest-side, var(--glow), transparent 72%); + pointer-events: none; + filter: blur(8px); +} +.hero-grid { position: relative; display: grid; gap: clamp(36px, 5vw, 64px); align-items: center; } + +/* layout: centered (default) */ +:root[data-hero="centered"] .hero-grid { grid-template-columns: 1fr; justify-items: center; text-align: center; } +:root[data-hero="centered"] .hero-cta { justify-content: center; } +:root[data-hero="centered"] .hero-meta { justify-content: center; } +:root[data-hero="centered"] .hero-copy { max-width: 720px; } +:root[data-hero="centered"] .hero-status { display: none; } + +/* layout: split (copy left, live panel right) */ +:root[data-hero="split"] .hero-grid { grid-template-columns: 1.1fr 0.9fr; } +:root[data-hero="split"] .hero-logo { max-width: 560px; } +:root[data-hero="split"] .hero-logo img { margin-inline: 0; } + +/* layout: spotlight (giant logo, minimal text, centered) */ +:root[data-hero="spotlight"] .hero-grid { grid-template-columns: 1fr; justify-items: center; text-align: center; } +:root[data-hero="spotlight"] .hero-cta { justify-content: center; } +:root[data-hero="spotlight"] .hero-meta { justify-content: center; } +:root[data-hero="spotlight"] .hero-logo { max-width: 900px; } +:root[data-hero="spotlight"] .hero-tagline { font-size: clamp(20px, 2.4vw, 28px); } +:root[data-hero="spotlight"] .hero-status { display: none; } +:root[data-hero="spotlight"] .hero-features-note { display: none; } + +.hero-logo { width: 100%; max-width: 760px; } +.hero-logo img { + width: 100%; + image-rendering: pixelated; + filter: drop-shadow(0 8px 0 oklch(0 0 0 / 0.45)) drop-shadow(0 18px 30px oklch(0 0 0 / 0.55)); + margin-inline: auto; +} +.hero-copy { display: flex; flex-direction: column; gap: 22px; } +.hero-tagline { + font-family: var(--font-head); + font-weight: 600; + font-size: clamp(24px, 3vw, 38px); + line-height: 1.08; + letter-spacing: 0.01em; + text-wrap: balance; +} +.hero-tagline .hl { color: var(--accent); } +.hero-sub { color: var(--muted); font-size: 18px; max-width: 52ch; margin: 0; } +.hero-cta { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; } +.hero-meta { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; color: var(--dim); font-size: 14.5px; } +.hero-meta .dot { width: 6px; height: 6px; background: var(--dim); } +.hero-ip-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; } + +/* status pill */ +.live-pill { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 7px 14px 7px 11px; + background: var(--slot); + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); + font-size: 14px; + color: var(--muted); + font-weight: 500; +} +.live-dot { + width: 9px; height: 9px; background: var(--accent); + box-shadow: 0 0 0 3px var(--glow); + animation: pulse 2.2s ease-in-out infinite; +} +@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} } +@media (prefers-reduced-motion: reduce){ .live-dot{ animation:none } } +.live-pill b { color: var(--text); font-variant-numeric: tabular-nums; } + +/* ============================================================ + SERVER-LIST STATUS PANEL + ============================================================ */ +.serverlist { + display: flex; + gap: 18px; + align-items: center; + background: var(--slot); + padding: 16px; + box-shadow: + inset 2px 2px 0 var(--bevel-hi), + inset -2px -2px 0 var(--bevel-lo), + 0 8px 24px oklch(0 0 0 / 0.4); +} +.serverlist .icon { + width: 72px; height: 72px; flex-shrink: 0; + display: grid; place-items: center; + background: var(--bg-2); + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); +} +.serverlist .icon .creeper { width: 48px; height: 48px; } +.serverlist .meta { flex: 1; min-width: 0; } +.serverlist .row1 { display: flex; align-items: baseline; gap: 12px; flex-wrap: nowrap; } +.serverlist .title { font-family: var(--font-head); font-weight: 600; font-size: 22px; white-space: nowrap; } +.serverlist .ver { color: var(--dim); font-family: var(--font-mono); font-size: 17px; white-space: nowrap; } +.serverlist .motd { margin: 6px 0 0; color: var(--muted); font-size: 15px; } +.serverlist .motd .g { color: var(--gold); } +.serverlist .motd .a { color: var(--accent); } +.serverlist .stat { text-align: right; flex-shrink: 0; } +.serverlist .players { font-variant-numeric: tabular-nums; font-size: 15px; color: var(--muted); display:flex; align-items:center; gap:8px; justify-content:flex-end; } +.serverlist .players b { color: var(--text); } + +/* signal bars */ +.bars { display: inline-flex; align-items: flex-end; gap: 2px; height: 16px; } +.bars i { width: 4px; background: var(--accent); image-rendering: pixelated; } +.bars i:nth-child(1){height:25%} +.bars i:nth-child(2){height:45%} +.bars i:nth-child(3){height:65%} +.bars i:nth-child(4){height:85%} +.bars i:nth-child(5){height:100%} + +/* stat tiles */ +.stat-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-top: 28px; +} +.tile { + background: var(--surface); + padding: 22px; + box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo); +} +.tile .k { font-family: var(--font-head); font-weight: 600; font-size: 38px; color: var(--text); line-height: 1; font-variant-numeric: tabular-nums; } +.tile .k .u { color: var(--accent); font-size: 22px; } +.tile .l { margin-top: 8px; color: var(--dim); font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; font-family: var(--font-pixel); font-size: 9px; line-height: 1.8; } + +/* ============================================================ + FEATURES + ============================================================ */ +.feat-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 18px; +} +.feat { + display: flex; + gap: 18px; + background: var(--surface); + padding: 26px; + box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo); + transition: transform .14s ease, filter .14s ease; +} +.feat:hover { transform: translateY(-3px); filter: brightness(1.06); } +.feat h3 { font-size: 21px; margin-bottom: 8px; } +.feat p { margin: 0; color: var(--muted); font-size: 16px; } + +/* pixel icon */ +.picon { + width: 52px; height: 52px; flex-shrink: 0; + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: repeat(7, 1fr); + background: var(--slot); + padding: 4px; + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); + image-rendering: pixelated; +} +.picon i { background: transparent; } +.picon i.on { background: var(--accent); } +.picon i.go { background: var(--gold); } + +/* ============================================================ + HOW TO JOIN — steps + ============================================================ */ +.steps { display: flex; flex-direction: column; gap: 20px; } +.step { + display: grid; + grid-template-columns: 92px 1fr; + gap: 28px; + background: var(--surface); + padding: 30px; + box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo); +} +.step .num { + width: 92px; height: 92px; + display: grid; place-items: center; + background: var(--slot); + font-family: var(--font-head); + font-weight: 600; + font-size: 46px; + color: var(--accent); + box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi); +} +.step .body { padding-top: 2px; } +.step .kicker { font-family: var(--font-pixel); font-size: 9px; letter-spacing: 0.1em; color: var(--dim); text-transform: uppercase; } +.step h3 { font-size: 26px; margin: 10px 0 12px; } +.step p { margin: 0 0 16px; color: var(--muted); font-size: 16.5px; max-width: 64ch; } +.step .actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; } +.step ul { margin: 0 0 16px; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 9px; } +.step ul li { display: flex; gap: 12px; color: var(--muted); font-size: 16px; } +.step ul li::before { content: ""; width: 10px; height: 10px; margin-top: 8px; flex-shrink: 0; background: var(--accent); } +.step .hint { color: var(--dim); font-size: 14px; } +.step kbd { + font-family: var(--font-mono); font-size: 18px; line-height: 1; + background: var(--bg-2); color: var(--text); + padding: 4px 8px; box-shadow: inset 1px 1px 0 var(--bevel-lo), inset -1px -1px 0 var(--bevel-hi); +} + +/* ============================================================ + FOOTER + ============================================================ */ +.footer { border-top: 1px solid var(--line); background: var(--bg-2); } +.footer-in { display: flex; flex-wrap: wrap; gap: 32px; align-items: center; justify-content: space-between; padding-block: 40px; } +.footer .brand .name { font-size: 20px; } +.footer-links { display: flex; gap: 8px; flex-wrap: wrap; } +.footer .disc { color: var(--dim); font-size: 13px; max-width: 46ch; } + +/* ---- floating dust ---- */ +.dust { position: absolute; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; } +@keyframes rise { + 0% { transform: translateY(0) translateX(0); opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { transform: translateY(-110vh) translateX(24px); opacity: 0; } +} +@media (prefers-reduced-motion: reduce) { .dust { display: none; } } +.hero-grid { z-index: 1; } + +/* ---- head-font: Press Start 2P is wide/tall — scale down ---- */ +:root[data-head="8bit"] .section-title { font-size: clamp(20px, 3vw, 38px); line-height: 1.25; } +:root[data-head="8bit"] .hero-tagline { font-size: clamp(15px, 2vw, 26px); line-height: 1.4; } +:root[data-head="8bit"] .brand .name { font-size: 15px; } +:root[data-head="8bit"] .mc-btn { font-size: 12px; } +:root[data-head="8bit"] .mc-btn.sm { font-size: 11px; } +:root[data-head="8bit"] .step h3 { font-size: 17px; line-height: 1.35; } +:root[data-head="8bit"] .step .num { font-size: 30px; } +:root[data-head="8bit"] .feat h3 { font-size: 15px; line-height: 1.4; } +:root[data-head="8bit"] .serverlist .title { font-size: 16px; } +:root[data-head="8bit"] .tile .k { font-size: 26px; } +:root[data-head="8bit"] .ip-chip button { font-size: 12px; } +:root[data-head="8bit"] .live-pill { font-size: 12px; } + +/* ---- head-font: Silkscreen is small-cap-ish — nudge ---- */ +:root[data-head="silkscreen"] .section-title { letter-spacing: 0; } + +/* ============================================================ + SCROLL REVEAL (fail-safe: visible at rest; animates only when JS adds .anim) + ============================================================ */ +.reveal { will-change: opacity, transform; } +@keyframes reveal-in { + from { opacity: 0; transform: translateY(22px); } + to { opacity: 1; transform: none; } +} +@media (prefers-reduced-motion: no-preference) { + .reveal.anim { animation: reveal-in .6s cubic-bezier(.2,.7,.3,1) both; } +} + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 880px) { + :root[data-hero="split"] .hero-grid { grid-template-columns: 1fr; } + :root[data-hero="split"] .hero-status { display: block; } + .stat-grid { grid-template-columns: repeat(2, 1fr); } + .feat-grid { grid-template-columns: 1fr; } + .nav-links { display: none; } + .serverlist { flex-wrap: wrap; } + .serverlist .stat { text-align: left; } +} +@media (max-width: 560px) { + .step { grid-template-columns: 1fr; gap: 18px; } + .step .num { width: 64px; height: 64px; font-size: 32px; } + .ip-chip .ip-val { font-size: 22px; } + .stat-grid { grid-template-columns: 1fr 1fr; } +} diff --git a/plan/09-landing.md b/plan/09-landing.md index d8924f4..2349e4e 100644 --- a/plan/09-landing.md +++ b/plan/09-landing.md @@ -2,32 +2,92 @@ ## Summary -Apex `ulicraft.local` serves a clean static page guiding guests through joining: +Apex `ulicraft.local` serves a static page guiding guests through joining: download launcher → add account → import modpack → connect. Built with **Astro + -TypeScript** in its own `./landing/` project; `npm run build` emits straight into -`./www/`, which Caddy already serves at the apex domain. No runtime JS framework — -output is plain HTML/CSS plus one tiny copy-to-clipboard script. +TypeScript** in `./landing/`; `pnpm run build` emits straight into `./www/`, +which Caddy/nginx serve at the apex. No runtime framework — output is plain +HTML/CSS plus one tiny vanilla script (copy-to-clipboard + scroll reveal). + +The visual is the **"Carved from stone"** pixel design ("Claude Design", +`landing/design/`): dark overworld palette, MC-GUI bevel buttons, pixel type, +creeper mark, server-list panel. That design was a React/Babel-via-CDN prototype +for a *fictional public SMP*; it was **ported to static Astro** with the real +modded-LAN content and offline-safe assets. ## Stack & wiring -- `landing/` — Astro project (own `package.json`, `tsconfig`). +- `landing/` — Astro project (own `package.json`, `tsconfig`). pnpm only. - `astro.config.mjs` → `outDir: "../www"`. Build overwrites `www/`; `www/` is gitignored (generated artifact). - `BASE_DOMAIN` env read at build time in `src/data/site.ts` (`process.env.BASE_DOMAIN ?? "ulicraft.local"`). Bake before deploy: `BASE_DOMAIN=ulicraft.local pnpm run build`. -- Logo: drop PNG at `landing/public/logo.png` → copied to `www/logo.png`, - referenced as `/logo.png` (favicon + hero). `image-rendering: pixelated` keeps - the stone 3D logo crisp. +- Logo: wide wordmark from the design at `landing/public/logo.png` (hero + favicon). + `image-rendering: pixelated` keeps it crisp. -## Content (src/data/site.ts is the single source) +### Source layout +``` +landing/ +├── design/ # "Claude Design" prototype (reference, not built) +├── public/ +│ ├── logo.png # hero wordmark (from design/assets/ulicraft-logo.png) +│ └── fonts/ # vendored woff2 + fonts.css (offline-safe) +└── src/ + ├── data/site.ts # single source: content + theme knobs + ├── styles/main.css # ported design system (mood/hero/bevels) + ├── components/ # Creeper, PixelIcon, CopyChip, ServerListPanel + └── pages/index.astro # composition + copy/reveal script + dust +``` -- Hero: logo, name, server address `mc.${BASE_DOMAIN}:25565` (copy button). -- Step 1: launcher downloads — 3 OS buttons → `/launcher/latest/`. -- Step 2: add authlib-injector account, URL `http://auth.${BASE_DOMAIN}/authlib-injector`, - register link to `http://auth.${BASE_DOMAIN}`. -- Step 3: import packwiz `http://packwiz.${BASE_DOMAIN}/pack.toml`. -- Step 4: connect to server address. +## Fonts — vendored for offline LAN + +The design pulled fonts from Google Fonts CDN; that breaks on offline LAN day. +`tooling/fetch-fonts.sh` downloads the woff2 + a URL-rewritten `fonts.css` into +`landing/public/fonts/`. `index.astro` links `/fonts/fonts.css`. Re-run the +script only if the font set changes (keep families in sync with `main.css` +`--font-*`): Pixelify Sans, Space Grotesk, Press Start 2P, Silkscreen, VT323. + +## Theme config (replaces the design's in-browser TweaksPanel) + +`site.ts` `theme` is baked at build — edit + rebuild to change: +- `mood`: `grass` (default) | `nether` | `end` +- `hero`: `centered` (default) | `split` | `spotlight` +- `headFont`: `pixelify` (default) | `8bit` | `silkscreen` +- `dust`: floating hero particles (CSS animation; respects reduced-motion) + +`index.astro` sets `` + `--font-head` from these. + +## Languages (i18n) + +Three locales, static, build-time, offline-safe — **English** (default, `/`), +**Spanish** (`/es/`), **Euskera** (`/eu/`). + +- One template `src/pages/[...lang].astro` with `getStaticPaths` emits all three + routes (`/`, `/es/`, `/eu/`). No runtime/JS routing. +- Translatable copy lives in `src/i18n/ui.ts` (`ui[locale]`), keyed identically + across locales. `site.ts` holds only language-neutral config (URLs, theme, + launcher files, stat/feature *structure*). +- Product / in-game literals never translated — `site.ts` `LITERALS` + (`authlib-injector`, `Multiplayer`, `Add Server`). OS names + version too. +- Sentences with links/kbd are split into parts (pre/post/link, pa/pb) so markup + stays in the template, not the strings. +- Nav has a pixel `EN · ES · EU` switcher (`.lang-switch`); active locale + highlighted. `` + `` set per page. +- Adding a locale: extend the `Lang` union + `ui`/`LANG_*` maps in `ui.ts`, add a + `getStaticPaths` entry. No template change. + +## Content (real modded-LAN flow, all in site.ts) + +- Hero: wordmark, eyebrow `Modded LAN · NeoForge 1.21.1`, tagline, sub, server + address `mc.${BASE_DOMAIN}` (copy chip), "How to Join". +- Status: static server-list panel (no fake live count — **option B**) + 4 honest + stat tiles (Mods / NeoForge ver / Player slots / Backups). +- Features ×4: kitchen-sink pack · self-hosted Drasl identity+skins · one-click + packwiz · built-to-last (homelab + 6h backups). +- How to Join ×4: **1** Fjord launcher (3 OS) → **2** authlib account + (`auth.${BASE_DOMAIN}/authlib-injector`) + register link + CA-cert note → + **3** packwiz import (`packwiz.${BASE_DOMAIN}/pack.toml`) → **4** connect. +- Footer: Mojang trademark disclaimer. ## Launcher download links (Option A — stable symlinks) @@ -38,16 +98,18 @@ Page links to fixed names so they survive launcher version bumps: `tooling/fetch-launcher.sh` must create these as symlinks under `mirror/launcher/latest/` pointing at the real versioned assets. **Keep the names -in sync** between `site.ts` and the script. Caddy mount -`./mirror/launcher:/srv/www/launcher:ro` makes them resolve at `/launcher/latest/…`. +in sync** between `site.ts` and the script. Mounted so they resolve at +`/launcher/latest/…` (see nginx/Caddy ingress). ## Tasks -- [ ] User: place logo at `landing/public/logo.png` -- [ ] `cd landing && pnpm install` -- [ ] `BASE_DOMAIN=ulicraft.local pnpm run build` → verify `www/index.html` + `www/logo.png` -- [ ] Extend `fetch-launcher.sh`: after download, symlink stable names → versioned files - (resolve which asset = win setup / mac dmg / linux AppImage) -- [ ] Confirm Caddy serves apex from `./www` and `/launcher/latest/*` from mirror -- [ ] Optional: pin launcher tag instead of `latest` for party day -- [ ] Optional: add a "what's in the pack" section once mod list firms up +- [x] Vendor fonts (`tooling/fetch-fonts.sh`) → `public/fonts/` +- [x] Port design → static Astro (components + main.css + index.astro) +- [x] Real content in `site.ts`; theme knobs replace TweaksPanel +- [x] i18n: en/es/eu via `[...lang].astro` + `i18n/ui.ts`; nav language switcher +- [x] `BASE_DOMAIN=ulicraft.local pnpm run build` → verified `www/` output + screenshot +- [ ] Extend `fetch-launcher.sh`: symlink stable names → versioned files +- [ ] Confirm ingress serves apex from `./www` and `/launcher/latest/*` from mirror +- [ ] Optional: try `mood: nether` / `hero: split` for party day +- [ ] Optional: wire real player count (option C) via mc-monitor JSON if wanted +``` diff --git a/tooling/fetch-fonts.sh b/tooling/fetch-fonts.sh new file mode 100755 index 0000000..ff74dcf --- /dev/null +++ b/tooling/fetch-fonts.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Vendor the landing-page web fonts locally so the apex site works on an +# offline LAN (no Google Fonts CDN at party time). +# +# Pulls woff2 + a rewritten @font-face stylesheet from Google Fonts into +# landing/public/fonts/. Re-run only when the font set changes. +# +# Halts on any error (cautious-by-default). +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +out="$here/landing/public/fonts" +mkdir -p "$out" + +# Modern Chrome UA → Google returns woff2 (not ttf). +UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" + +# Families + weights used by the landing (keep in sync with main.css --font-*). +CSS_URL="https://fonts.googleapis.com/css2" +CSS_URL+="?family=Pixelify+Sans:wght@600;700" +CSS_URL+="&family=Space+Grotesk:wght@400;500;600;700" +CSS_URL+="&family=Press+Start+2P" +CSS_URL+="&family=Silkscreen:wght@400;700" +CSS_URL+="&family=VT323" +CSS_URL+="&display=swap" + +echo "→ fetching @font-face CSS" +raw="$(curl -fsSL -A "$UA" "$CSS_URL")" + +echo "→ downloading woff2 files" +# Pull every gstatic woff2 URL, download to out/, dedupe by basename. +echo "$raw" | grep -oE 'https://fonts\.gstatic\.com/[^)]+\.woff2' | sort -u | while read -r url; do + fn="$(basename "$url")" + if [ ! -f "$out/$fn" ]; then + curl -fsSL -o "$out/$fn" "$url" + echo " $fn" + fi +done + +echo "→ writing fonts.css (URLs rewritten to /fonts/…)" +# Rewrite absolute gstatic URLs to local /fonts/. +echo "$raw" | sed -E 's#https://fonts\.gstatic\.com/[^)]+/([^)/]+\.woff2)#/fonts/\1#g' > "$out/fonts.css" + +echo "✓ fonts vendored into $out"