feat(landing): pixel design ported to static Astro + en/es/eu i18n
Adopt the "Carved from stone" pixel design (landing/design/) as the apex onboarding page, ported from its React/Babel-via-CDN prototype to static Astro so it works on an offline LAN. Real modded-LAN content replaces the prototype's fictional public-SMP copy. - Vendor web fonts locally (tooling/fetch-fonts.sh -> public/fonts/) so the page renders without the Google Fonts CDN at party time. - Port the design system (main.css: mood/hero/bevels) and split markup into Astro components (Creeper, PixelIcon, CopyChip, ServerListPanel). - site.ts holds language-neutral config + theme knobs (mood/hero/headFont/ dust) that replace the design's in-browser TweaksPanel; LITERALS holds never-translated product/in-game terms. - i18n: English (/), Spanish (/es/), Euskera (/eu/) generated from one [...lang].astro via getStaticPaths; copy lives in src/i18n/ui.ts. Nav has an EN/ES/EU switcher; html lang + hreflang set per page. - Status panel shows a static server-list row + honest stat tiles (no fake live count). Copy + scroll-reveal are the only client JS. Build emits to ../www (gitignored). Euskera strings pending a native review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
landing/src/components/CopyChip.astro
Normal file
17
landing/src/components/CopyChip.astro
Normal file
@@ -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;
|
||||
---
|
||||
<div class:list={["ip-chip", variant === "url" && "url"]}>
|
||||
<span class="ip-val"><span class="pin">▸</span>{value}</span>
|
||||
<button type="button" data-copy={value} data-label={label} data-copied={copiedLabel}>{label}</button>
|
||||
</div>
|
||||
17
landing/src/components/Creeper.astro
Normal file
17
landing/src/components/Creeper.astro
Normal file
@@ -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("");
|
||||
---
|
||||
<div class="creeper" aria-hidden="true">
|
||||
{cells.map((c) => <i class={c === "1" ? "f" : undefined} />)}
|
||||
</div>
|
||||
20
landing/src/components/PixelIcon.astro
Normal file
20
landing/src/components/PixelIcon.astro
Normal file
@@ -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<Props["glyph"], string[]> = {
|
||||
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";
|
||||
---
|
||||
<div class="picon" aria-hidden="true">
|
||||
{cells.map((c) => <i class={c === "1" ? onClass : undefined} />)}
|
||||
</div>
|
||||
31
landing/src/components/ServerListPanel.astro
Normal file
31
landing/src/components/ServerListPanel.astro
Normal file
@@ -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;
|
||||
---
|
||||
<div class="serverlist">
|
||||
<div class="icon"><Creeper /></div>
|
||||
<div class="meta">
|
||||
<div class="row1">
|
||||
<span class="title">{site.name}</span>
|
||||
<span class="ver">Java {site.mcVersion}</span>
|
||||
</div>
|
||||
<p class="motd">
|
||||
<span class="a">⛏ {modded}</span> · {motd}
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="players">
|
||||
<span class="bars" aria-hidden="true"><i /><i /><i /><i /><i /></span>
|
||||
<span>{upTo} <b>{status.slots}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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.<domain>) 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<string, string> = {
|
||||
pixelify: "'Pixelify Sans', system-ui, sans-serif",
|
||||
"8bit": "'Press Start 2P', monospace",
|
||||
silkscreen: "'Silkscreen', system-ui, sans-serif",
|
||||
};
|
||||
|
||||
340
landing/src/i18n/ui.ts
Normal file
340
landing/src/i18n/ui.ts
Normal file
@@ -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<Lang, string> = { en: "EN", es: "ES", eu: "EU" };
|
||||
// URL path per locale (en is default, served at root).
|
||||
export const LANG_PATH: Record<Lang, string> = { 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<Lang, UI> = {
|
||||
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.",
|
||||
},
|
||||
},
|
||||
};
|
||||
317
landing/src/pages/[...lang].astro
Normal file
317
landing/src/pages/[...lang].astro
Normal file
@@ -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`,
|
||||
};
|
||||
});
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html
|
||||
lang={t.htmlLang}
|
||||
data-mood={theme.mood}
|
||||
data-hero={theme.hero}
|
||||
data-head={theme.headFont}
|
||||
style={`--font-head: ${headFont}`}
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{site.name} — {t.hero.howToJoin}</title>
|
||||
<meta name="description" content={`${site.name}: ${t.hero.tagline}`} />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_PATH[l]} />)}
|
||||
</head>
|
||||
<body>
|
||||
<!-- NAV -->
|
||||
<header class="nav">
|
||||
<div class="wrap nav-in">
|
||||
<a class="brand" href="#top">
|
||||
<Creeper />
|
||||
<span class="name">ULICRAFT</span>
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="#status">{t.nav.status}</a>
|
||||
<a href="#features">{t.nav.features}</a>
|
||||
<a href="#join">{t.nav.join}</a>
|
||||
</nav>
|
||||
<span class="nav-spacer"></span>
|
||||
<div class="lang-switch" aria-label="Language">
|
||||
{LANGS.map((l) => (
|
||||
<a href={LANG_PATH[l]} class:list={[l === locale && "on"]} aria-current={l === locale ? "true" : undefined}>{LANG_LABEL[l]}</a>
|
||||
))}
|
||||
</div>
|
||||
<a class="mc-btn primary sm" href="#join">{t.nav.play}</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top">
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
{theme.dust && (
|
||||
<div class="dust" aria-hidden="true">
|
||||
{dustBits.map((b) => (
|
||||
<i
|
||||
style={`position:absolute;left:${b.left};bottom:-20px;width:${b.size};height:${b.size};background:${b.bg};opacity:${b.opacity};image-rendering:pixelated;animation:${b.anim}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div class="wrap hero-grid">
|
||||
<div class="hero-logo">
|
||||
<img src="/logo.png" alt={site.name} width="707" height="148" />
|
||||
</div>
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow">{t.hero.eyebrow} {site.mcVersion}</span>
|
||||
<h1 class="hero-tagline">{t.hero.tagline}</h1>
|
||||
<p class="hero-sub">{t.hero.sub}</p>
|
||||
<div class="hero-ip-row">
|
||||
<CopyChip value={site.serverAddress} variant="ip" label={t.hero.copyIp} copiedLabel={t.copied} />
|
||||
<a class="mc-btn" href="#join">{t.hero.howToJoin}</a>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span>Java {site.mcVersion}</span>
|
||||
<span class="dot"></span>
|
||||
<span>{t.hero.metaModpack}</span>
|
||||
<span class="dot"></span>
|
||||
<span>{t.hero.metaLan}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-status">
|
||||
<ServerListPanel
|
||||
modded={t.status.modded}
|
||||
motd={t.status.motd}
|
||||
upTo={t.status.upTo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- STATUS -->
|
||||
<section id="status" class="pad">
|
||||
<div class="wrap">
|
||||
<div class="sec-head reveal">
|
||||
<span class="eyebrow">{t.status.eyebrow}</span>
|
||||
<h2 class="section-title">{t.status.title}</h2>
|
||||
<p class="lead">{t.status.lead}</p>
|
||||
</div>
|
||||
<div class="reveal">
|
||||
<ServerListPanel
|
||||
modded={t.status.modded}
|
||||
motd={t.status.motd}
|
||||
upTo={t.status.upTo}
|
||||
/>
|
||||
</div>
|
||||
<div class="stat-grid">
|
||||
{site.stats.map((s, i) => (
|
||||
<div class="tile reveal" style={`transition-delay:${i * 60}ms`}>
|
||||
<div class="k">{s.key}</div>
|
||||
<div class="l">{t.statLabels[i]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section id="features" class="pad" style="background: var(--bg-2)">
|
||||
<div class="wrap">
|
||||
<div class="sec-head reveal">
|
||||
<span class="eyebrow">{t.features.eyebrow}</span>
|
||||
<h2 class="section-title">{t.features.title}</h2>
|
||||
<p class="lead">{t.features.lead}</p>
|
||||
</div>
|
||||
<div class="feat-grid">
|
||||
{site.features.map((f, i) => (
|
||||
<div class="feat reveal" style={`transition-delay:${(i % 2) * 80}ms`}>
|
||||
<PixelIcon glyph={f.glyph} gold={f.gold} />
|
||||
<div>
|
||||
<h3>{t.features.items[i].h}</h3>
|
||||
<p>{t.features.items[i].p}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW TO JOIN -->
|
||||
<section id="join" class="pad">
|
||||
<div class="wrap">
|
||||
<div class="sec-head reveal">
|
||||
<span class="eyebrow">{t.join.eyebrow}</span>
|
||||
<h2 class="section-title">{t.join.title}</h2>
|
||||
<p class="lead">{t.join.lead}</p>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<!-- STEP 1 -->
|
||||
<div class="step reveal">
|
||||
<div class="num">1</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s1.kicker}</span>
|
||||
<h3>{t.join.s1.h.replace("{name}", launcherName)}</h3>
|
||||
<p>{t.join.s1.p.replace(/\{name\}/g, launcherName)}</p>
|
||||
<div class="actions">
|
||||
{launcher.downloads.map((d) => (
|
||||
<a class="mc-btn sm" href={`${launcher.base}/${d.file}`}>
|
||||
{d.os} <span class="hint">· {d.hint}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2 -->
|
||||
<div class="step reveal">
|
||||
<div class="num">2</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s2.kicker}</span>
|
||||
<h3>{t.join.s2.h}</h3>
|
||||
<p>
|
||||
{t.join.s2.pa} <strong style="color:var(--text)">{LITERALS.authlib}</strong> {t.join.s2.pb}
|
||||
</p>
|
||||
<div class="actions" style="margin-bottom:14px">
|
||||
<CopyChip value={site.authlibUrl} variant="url" label={t.join.s2.copy} copiedLabel={t.copied} />
|
||||
</div>
|
||||
<p>
|
||||
{t.join.s2.registerPre} <a href={site.authUrl}>{site.authUrl}</a>.
|
||||
</p>
|
||||
<span class="hint">
|
||||
{t.join.s2.caPre} <a href={site.caCertUrl}>{t.join.s2.caLink}</a> {t.join.s2.caPost}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3 -->
|
||||
<div class="step reveal">
|
||||
<div class="num">3</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s3.kicker}</span>
|
||||
<h3>{t.join.s3.h}</h3>
|
||||
<p>{t.join.s3.p}</p>
|
||||
<div class="actions">
|
||||
<CopyChip value={site.packwizUrl} variant="url" label={t.join.s2.copy} copiedLabel={t.copied} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4 -->
|
||||
<div class="step reveal">
|
||||
<div class="num">4</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s4.kicker}</span>
|
||||
<h3>{t.join.s4.h}</h3>
|
||||
<p>
|
||||
{t.join.s4.pa} <kbd>{LITERALS.multiplayer}</kbd> → <kbd>{LITERALS.addServer}</kbd>{t.join.s4.pb}
|
||||
</p>
|
||||
<div class="actions">
|
||||
<CopyChip value={site.serverAddress} variant="ip" label={t.join.s4.copy} copiedLabel={t.copied} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer">
|
||||
<div class="wrap footer-in">
|
||||
<a class="brand" href="#top">
|
||||
<Creeper />
|
||||
<span class="name">ULICRAFT</span>
|
||||
</a>
|
||||
<div class="footer-links">
|
||||
<a class="mc-btn primary sm" href="#join">{t.footer.join}</a>
|
||||
</div>
|
||||
<p class="disc">
|
||||
{t.footer.disc} {site.name} · {site.baseDomain}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Copy-to-clipboard for every [data-copy] button (HTTP-LAN safe fallback).
|
||||
function copyText(text: string) {
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
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 {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
document.querySelectorAll<HTMLButtonElement>("button[data-copy]").forEach((btn) => {
|
||||
let timer: number | undefined;
|
||||
btn.addEventListener("click", () => {
|
||||
copyText(btn.dataset.copy ?? "");
|
||||
const label = btn.dataset.label ?? "Copy";
|
||||
btn.textContent = btn.dataset.copied ?? "Copied!";
|
||||
btn.classList.add("copied");
|
||||
clearTimeout(timer);
|
||||
timer = window.setTimeout(() => {
|
||||
btn.textContent = label;
|
||||
btn.classList.remove("copied");
|
||||
}, 1600);
|
||||
});
|
||||
});
|
||||
|
||||
// Scroll reveal — visible at rest; .anim added when scrolled into view.
|
||||
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
const els = [...document.querySelectorAll<HTMLElement>(".reveal")];
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const e of entries) {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add("anim");
|
||||
io.unobserve(e.target);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0px 0px -10% 0px" }
|
||||
);
|
||||
els.forEach((el) => io.observe(el));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,199 +0,0 @@
|
||||
---
|
||||
import { site } from "../data/site";
|
||||
const { launcher } = site;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{site.name} — join the server</title>
|
||||
<meta name="description" content={`${site.name}: ${site.tagline}`} />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header class="hero">
|
||||
<img class="logo" src="/logo.png" alt={`${site.name} logo`} width="128" height="128" />
|
||||
<h1>{site.name}</h1>
|
||||
<p class="tagline">{site.tagline}</p>
|
||||
<p class="addr">
|
||||
<span class="addr-label">Server</span>
|
||||
<code id="server-addr">{site.serverAddress}</code>
|
||||
<button class="copy" data-copy={site.serverAddress} type="button">Copy</button>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ol class="steps">
|
||||
<li>
|
||||
<h2><span class="num">1</span> Download the launcher</h2>
|
||||
<p>Grab <strong>{launcher.name}</strong> for your system.</p>
|
||||
<div class="downloads">
|
||||
{launcher.downloads.map((d) => (
|
||||
<a class="dl" href={`${launcher.base}/${d.file}`}>
|
||||
<span class="dl-os">{d.os}</span>
|
||||
<span class="dl-hint">{d.hint}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h2><span class="num">2</span> Add your account</h2>
|
||||
<p>In the launcher, add an <strong>authlib-injector</strong> account using this URL:</p>
|
||||
<p>
|
||||
<code>{site.authlibUrl}</code>
|
||||
<button class="copy" data-copy={site.authlibUrl} type="button">Copy</button>
|
||||
</p>
|
||||
<p>
|
||||
Need credentials? Register at <a href={site.authUrl}>{site.authUrl}</a>, then log in
|
||||
with that username and password.
|
||||
</p>
|
||||
<p class="note">
|
||||
Offline party? Download and trust the
|
||||
<a href={site.caCertUrl}>local CA certificate</a> so the game-file
|
||||
mirror works over HTTPS.
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h2><span class="num">3</span> Import the modpack</h2>
|
||||
<p>Add a new instance from this packwiz URL:</p>
|
||||
<p>
|
||||
<code>{site.packwizUrl}</code>
|
||||
<button class="copy" data-copy={site.packwizUrl} type="button">Copy</button>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h2><span class="num">4</span> Connect</h2>
|
||||
<p>Launch the instance, add a server, and join:</p>
|
||||
<p>
|
||||
<code>{site.serverAddress}</code>
|
||||
<button class="copy" data-copy={site.serverAddress} type="button">Copy</button>
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<footer>
|
||||
<p>{site.name} · {site.baseDomain}</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Copy-to-clipboard for every [data-copy] button.
|
||||
document.querySelectorAll<HTMLButtonElement>("button.copy").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const text = btn.dataset.copy ?? "";
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const prev = btn.textContent;
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(() => (btn.textContent = prev), 1200);
|
||||
} catch {
|
||||
btn.textContent = "Copy failed";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--stone: #7a7a7a;
|
||||
--bg: #f4f4f2;
|
||||
--card: #ffffff;
|
||||
--ink: #1c1c1c;
|
||||
--muted: #6b6b6b;
|
||||
--line: #e2e2df;
|
||||
--accent: #4f7942;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
line-height: 1.55;
|
||||
}
|
||||
main {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 1.25rem 4rem;
|
||||
}
|
||||
.hero { text-align: center; margin-bottom: 2.5rem; }
|
||||
.logo {
|
||||
image-rendering: pixelated;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
h1 { font-size: 2.4rem; margin: 0.25rem 0 0; letter-spacing: -0.5px; }
|
||||
.tagline { color: var(--muted); margin: 0.25rem 0 1.25rem; }
|
||||
.addr {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.addr-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--muted);
|
||||
}
|
||||
code {
|
||||
background: #ededea;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.copy {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--card);
|
||||
border-radius: 6px;
|
||||
padding: 0.2rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.copy:hover { border-color: var(--stone); }
|
||||
.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 1rem; }
|
||||
.steps > li {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
.steps h2 { font-size: 1.2rem; margin: 0 0 0.5rem; display: flex; align-items: center; gap: 0.6rem; }
|
||||
.num {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 1.7rem;
|
||||
height: 1.7rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.steps p { margin: 0.4rem 0; }
|
||||
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; margin-top: 0.75rem; }
|
||||
.dl {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
.dl:hover { border-color: var(--accent); }
|
||||
.dl-os { font-weight: 600; }
|
||||
.dl-hint { font-size: 0.8rem; color: var(--muted); }
|
||||
footer { text-align: center; color: var(--muted); font-size: 0.85rem; margin-top: 2.5rem; }
|
||||
a { color: var(--accent); }
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
608
landing/src/styles/main.css
Normal file
608
landing/src/styles/main.css
Normal file
@@ -0,0 +1,608 @@
|
||||
/* ============================================================
|
||||
ULICRAFT — "Carved from stone"
|
||||
Design system: dark overworld, MC-GUI bevels, pixel type
|
||||
============================================================ */
|
||||
|
||||
/* ---- Mood palettes (data-mood on <html>) ---- */
|
||||
: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; }
|
||||
}
|
||||
Reference in New Issue
Block a user