Add white label template for starter

This commit is contained in:
Achim Rohn
2025-09-12 23:26:10 +02:00
parent a817f7550b
commit 35ee289765
8 changed files with 874 additions and 22 deletions
+2 -2
View File
@@ -4,13 +4,14 @@ go 1.25.0
require (
ersteller-lib v0.0.0
github.com/jackc/pgx/v5 v5.7.6
github.com/joho/godotenv v1.5.1
maragu.dev/gomponents v1.2.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/labstack/echo/v4 v4.13.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
@@ -24,7 +25,6 @@ require (
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
maragu.dev/gomponents v1.2.0 // indirect
maragu.dev/gomponents-htmx v0.6.1 // indirect
)
-16
View File
@@ -5,8 +5,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
@@ -21,8 +19,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ=
github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -36,33 +32,21 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U=
maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
maragu.dev/gomponents-htmx v0.6.1 h1:vXXOkvqEDKYxSwD1UwqmVp12YwFSuM6u8lsRn7Evyng=
+111
View File
@@ -0,0 +1,111 @@
package index
import (
. "ersteller-lib"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
const IndexPath = "/"
const IndexPathDe = "/"
var indexTexts *IndexTexts
type IndexTexts struct {
PageTitle I18nText
PageDescription I18nText
WelcomeTitle I18nText
WelcomeText I18nText
FeaturesTitle I18nText
FeaturesText I18nText
ContactTitle I18nText
ContactText I18nText
}
type Page struct {
createPage CreateHtmxPageFunc
ViewRoute HtmxRoute
}
func NewPage(createPage CreateHtmxPageFunc, server HtmxServer, path *ActivePath) *Page {
if indexTexts == nil {
createIndexTexts()
}
page := &Page{
createPage: createPage,
}
page.ViewRoute = NewHtmxGetRoute(page.View, LanguagePaths{
En: IndexPath,
De: IndexPathDe,
}).SetActivePath(path)
page.ViewRoute.Add(server)
return page
}
func createIndexTexts() {
indexTexts = &IndexTexts{
PageTitle: NewI18nText(map[Language]string{
En: "Home",
De: "Startseite",
}),
PageDescription: NewI18nText(map[Language]string{
En: "Welcome to our application - Your digital solution",
De: "Willkommen bei unserer Anwendung - Ihre digitale Lösung",
}),
WelcomeTitle: NewI18nText(map[Language]string{
En: "Welcome to Your Application",
De: "Willkommen bei Ihrer Anwendung",
}),
WelcomeText: NewI18nText(map[Language]string{
En: "This is your white label template starter kit. Customize this content to match your brand and requirements.",
De: "Dies ist Ihr White-Label-Template-Starter-Kit. Passen Sie diesen Inhalt an Ihre Marke und Anforderungen an.",
}),
FeaturesTitle: NewI18nText(map[Language]string{
En: "Key Features",
De: "Hauptfunktionen",
}),
FeaturesText: NewI18nText(map[Language]string{
En: "Built with modern web technologies, responsive design, and multi-language support. Perfect foundation for your next project.",
De: "Entwickelt mit modernen Web-Technologien, responsivem Design und mehrsprachiger Unterstützung. Perfekte Grundlage für Ihr nächstes Projekt.",
}),
ContactTitle: NewI18nText(map[Language]string{
En: "Get Started",
De: "Erste Schritte",
}),
ContactText: NewI18nText(map[Language]string{
En: "Ready to customize this template? Replace this placeholder content with your own information and branding.",
De: "Bereit, diese Vorlage anzupassen? Ersetzen Sie diesen Platzhalter-Inhalt durch Ihre eigenen Informationen und Ihr Branding.",
}),
}
}
func (p *Page) getMetaData() PageWebsiteMetaData {
return PageWebsiteMetaData{
Title: indexTexts.PageTitle,
Lang: En,
Description: indexTexts.PageDescription,
}
}
func (p *Page) View(c HtmxContext) {
content := IndexContent(c.GetLanguage())
p.createPage(c, p.getMetaData(), content)
}
func IndexContent(language Language) Group {
return []Node{
Div(Class("hero-section"),
H1(Class("hero-title"), Text(indexTexts.WelcomeTitle.FromLang(language))),
P(Class("hero-description"), Text(indexTexts.WelcomeText.FromLang(language))),
),
Div(Class("content-section"),
H2(Class("section-title"), Text(indexTexts.FeaturesTitle.FromLang(language))),
P(Class("section-description"), Text(indexTexts.FeaturesText.FromLang(language))),
),
Div(Class("content-section"),
H2(Class("section-title"), Text(indexTexts.ContactTitle.FromLang(language))),
P(Class("section-description"), Text(indexTexts.ContactText.FromLang(language))),
),
}
}
+4 -4
View File
@@ -3,13 +3,13 @@ package main
import (
. "ersteller-lib"
"ersteller-lib/starter/env"
"ersteller-lib/starter/routes"
"log"
"net/http"
)
func main() {
GlobalI18n = GlobalI18nImplementation{}
server := NewHtmxServer()
environment := env.LoadEnvironment()
@@ -20,7 +20,7 @@ func main() {
}
defer db.Close()
Debug("starting rest api on port 8089")
serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler)
log.Fatal(http.ListenAndServe(":8090", serverWithMiddleWare))
Debug("starting white label app on port 8090")
handler := routes.CreateApi(db)
log.Fatal(http.ListenAndServe(":8090", handler))
}
+226
View File
@@ -0,0 +1,226 @@
package page
import (
. "ersteller-lib"
"fmt"
"time"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
const DeIndexUrl = "/de/index"
const EnIndexUrl = "/en/index"
const DefaultLanguage = En
var _texts *Texts
func CreatePage(req HtmxContext, metadata PageWebsiteMetaData, content ...Node) {
metadata.Lang = req.GetLanguage()
var contentForFunction Node
if len(content) > 0 {
contentForFunction = Div(content...)
} else if len(content) == 0 {
contentForFunction = Div()
} else if len(content) == 1 {
contentForFunction = content[0]
}
styles := []string{
"/static/styles.css",
}
for _, src := range metadata.StyleSrcs {
styles = append(styles, src)
}
scripts := []string{
"/static/scripts/language-select.js",
"/static/htmx.js",
}
for _, src := range metadata.ScriptSrcs {
scripts = append(scripts, src)
}
page := HTML5(HTML5Props{
Title: "White Label App " + metadata.Title.From(req),
Language: string(metadata.Lang),
Description: metadata.Description.From(req),
Head: []Node{
Map(styles, func(s string) Node {
return Link(Rel("stylesheet"), Href(s))
}),
Map(scripts, func(s string) Node {
return Script(Type("text/javascript"), Src(s))
}),
addLanguageSelectScript(metadata),
},
Body: []Node{
Header(Class("header"),
Div(Class("header-content"),
Div(Class("logo"),
Span(Class("logo-icon"), Text("🚀")),
Span(Class("logo-text"), Text("White Label App")),
),
getNav(req, metadata),
getLanguageSwitcher(req, metadata),
),
),
Div(Class("container"),
contentForFunction,
),
Footer(Class("footer"),
Div(Class("footer-menu"), Aria("label", "Footer Menu"),
getFooterMenu(req, metadata),
),
P(Class("footer-disclaimer"), Text(getTexts().disclaimer.From(req))),
P(Text(getTexts().getCopyright(req.GetLanguage()))),
),
},
})
req.Render(page)
}
func addLanguageSelectScript(metadata PageWebsiteMetaData) Node {
script := InlineTemplate(`
(function() {
const langs = {
de: "$.DeUrl$",
en: "$.EnUrl$",
};
const currentLang = "$.CurrentLang$";
const defaultLang = "$.DefaultLang$";
selectWebsiteLanguage(currentLang, langs, defaultLang);
})();`, struct {
CurrentLang string
DeUrl string
EnUrl string
DefaultLang string
}{
CurrentLang: string(metadata.Lang),
DeUrl: DeIndexUrl,
EnUrl: EnIndexUrl,
DefaultLang: string(DefaultLanguage),
})
return Script(Type("text/javascript"), Raw(script))
}
func getNav(c HtmxContext, metadata PageWebsiteMetaData) Node {
return Nav(Class("nav"), Aria("label", "Main Menu"),
Map(metadata.NavItems, func(path ActivePath) Node {
return createNavItem(c, path)
}),
)
}
func createNavItem(c HtmxContext, activePath ActivePath) Node {
isActive := activePath.IsActive(c)
return A(
Href(activePath.GetPath(c.GetLanguage())),
Text(activePath.From(c)),
If(isActive, Attr("aria-current", "page")),
If(isActive, Class("selected")),
)
}
func getLanguageSwitcher(c HtmxContext, metadata PageWebsiteMetaData) Node {
currentLang := c.GetLanguage()
var currentRoute HtmxRoute
for _, route := range c.GetAllRoutes() {
if route.IsCurrentRoute(c) {
currentRoute = route
break
}
}
return Div(Class("language-switcher"),
createLanguageButton(En, currentLang, currentRoute, c),
createLanguageButton(De, currentLang, currentRoute, c),
)
}
func getFooterMenu(c HtmxContext, metadata PageWebsiteMetaData) Node {
lang := c.GetLanguage()
return Div(
Map(metadata.FooterNavItems, func(path ActivePath) Node {
return Span(
A(Href(path.GetPath(lang)), Class("footer-link"), Text(path.From(c))),
)
}),
)
}
func createLanguageButton(lang Language, currentLang Language, route HtmxRoute, c HtmxContext) Node {
isActive := lang == currentLang
var href string
if route != nil {
href = route.ToUrlFromContext(c, lang)
} else {
// Fallback to index page if no active path found
if lang == En {
href = EnIndexUrl
} else {
href = DeIndexUrl
}
}
var langText string
if lang == En {
langText = "EN"
} else {
langText = "DE"
}
// Combine classes properly
var classes string
if isActive {
classes = "language-button active"
} else {
classes = "language-button inactive"
}
return A(
Href(href),
Text(langText),
Class(classes),
)
}
type Texts struct {
disclaimer I18nText
copyright I18nText
}
func NewTexts() *Texts {
return &Texts{
disclaimer: NewI18nText(map[Language]string{
De: "Dies ist eine White-Label-Vorlage. Passen Sie den Inhalt an Ihre Bedürfnisse an.",
En: "This is a white label template. Customize the content to fit your needs.",
}),
copyright: NewI18nText(map[Language]string{
De: "© 2020 - $Year$ White Label App. Alle Rechte vorbehalten.",
En: "© 2020 - $Year$ White Label App. All rights reserved.",
}),
}
}
func (t *Texts) getCopyright(lang Language) string {
currentYear := fmt.Sprint(time.Now().Year())
text := t.copyright.FromLang(lang)
return InlineTemplate(text, struct {
Year string
}{
Year: currentYear,
})
}
func getTexts() *Texts {
if _texts != nil {
return _texts
}
_texts = NewTexts()
return _texts
}
+112
View File
@@ -0,0 +1,112 @@
package routes
import (
. "ersteller-lib"
"ersteller-lib/starter/index"
"ersteller-lib/starter/page"
"net/http"
"github.com/jackc/pgx/v5/pgxpool"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func CreateApi(db *pgxpool.Pool) http.Handler {
server := NewHtmxServer()
HtmxRouteDebugTrace = true
indexActivePath := NewActivePath(map[Language]string{
En: "Home",
De: "Startseite",
}, LanguagePaths{
En: index.IndexPath,
De: index.IndexPathDe,
})
aboutActivePath := NewActivePath(map[Language]string{
En: "About",
De: "Über uns",
}, LanguagePaths{
En: "/about",
De: "/de/ueber-uns",
})
contactActivePath := NewActivePath(map[Language]string{
En: "Contact",
De: "Kontakt",
}, LanguagePaths{
En: "/contact",
De: "/de/kontakt",
})
// Main navigation items
activePaths := []ActivePath{indexActivePath, aboutActivePath, contactActivePath}
// Footer navigation items (placeholder - can be customized)
footerPaths := []ActivePath{}
createPageFunc := createPage(activePaths, footerPaths)
indexPage := index.NewPage(createPageFunc, server, &indexActivePath)
server.HandleStaticAndDefaultPath(indexPage.ViewRoute, En)
// Add placeholder routes for About and Contact pages
// These can be implemented as needed
aboutRoute := NewHtmxGetRoute(func(c HtmxContext) {
content := []Node{
Div(Class("hero-section"),
H1(Class("hero-title"), Text("About Us")),
P(Class("hero-description"), Text("This is a placeholder about page. Customize this content to tell your story.")),
),
}
createPageFunc(c, PageWebsiteMetaData{
Title: NewI18nText(map[Language]string{
En: "About",
De: "Über uns",
}),
Description: NewI18nText(map[Language]string{
En: "Learn more about us",
De: "Erfahren Sie mehr über uns",
}),
}, content...)
}, LanguagePaths{
En: "/about",
De: "/de/ueber-uns",
}).SetActivePath(&aboutActivePath)
aboutRoute.Add(server)
contactRoute := NewHtmxGetRoute(func(c HtmxContext) {
content := []Node{
Div(Class("hero-section"),
H1(Class("hero-title"), Text("Contact Us")),
P(Class("hero-description"), Text("This is a placeholder contact page. Add your contact information and forms here.")),
),
}
createPageFunc(c, PageWebsiteMetaData{
Title: NewI18nText(map[Language]string{
En: "Contact",
De: "Kontakt",
}),
Description: NewI18nText(map[Language]string{
En: "Get in touch with us",
De: "Nehmen Sie Kontakt mit uns auf",
}),
}, content...)
}, LanguagePaths{
En: "/contact",
De: "/de/kontakt",
}).SetActivePath(&contactActivePath)
contactRoute.Add(server)
serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler)
return serverWithMiddleWare
}
func createPage(activePaths []ActivePath, footerPaths []ActivePath) CreateHtmxPageFunc {
return func(req HtmxContext, metadata PageWebsiteMetaData, content ...Node) {
metadata.NavItems = activePaths
metadata.FooterNavItems = footerPaths
page.CreatePage(req, metadata, content...)
}
}
+39
View File
@@ -0,0 +1,39 @@
// Language selection functionality for the white label template
function selectWebsiteLanguage(currentLang, langs, defaultLang) {
// This function handles language switching for the website
// It can be extended to handle URL routing and language persistence
console.log('Current language:', currentLang);
console.log('Available languages:', langs);
console.log('Default language:', defaultLang);
// Add any additional language switching logic here
// For example, saving language preference to localStorage
if (typeof Storage !== 'undefined') {
localStorage.setItem('preferredLanguage', currentLang);
}
}
// Function to get preferred language from storage
function getPreferredLanguage() {
if (typeof Storage !== 'undefined') {
return localStorage.getItem('preferredLanguage');
}
return null;
}
// Initialize language selection on page load
document.addEventListener('DOMContentLoaded', function() {
const preferredLang = getPreferredLanguage();
if (preferredLang) {
console.log('Preferred language from storage:', preferredLang);
}
});
// Export functions for potential module use
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
selectWebsiteLanguage,
getPreferredLanguage
};
}
+380
View File
@@ -0,0 +1,380 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
color: #333333;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon points="0,100 50,0 100,100" fill="rgba(255,255,255,0.1)"/></svg>') repeat-x;
background-size: 60px 100%;
opacity: 0.3;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 2;
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo-icon {
font-size: 2.5rem;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-text {
font-size: 2rem;
font-weight: bold;
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.nav {
display: flex;
gap: 30px;
}
.nav a {
color: #ffffff;
text-decoration: none;
font-weight: bold;
padding: 10px 15px;
border-radius: 5px;
transition: all 0.3s ease;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
}
.nav a:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.nav a.selected {
background: linear-gradient(45deg, #667eea, #764ba2);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.language-switcher {
display: flex;
gap: 3px;
align-items: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.language-button {
color: #ffffff;
text-decoration: none;
font-weight: bold;
padding: 8px 14px;
border-radius: 6px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
font-size: 0.85rem;
min-width: 36px;
text-align: center;
display: block;
}
.language-button.active {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.language-button.inactive:hover {
background: rgba(255, 255, 255, 0.15);
transform: scale(1.02);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.hero-section {
text-align: center;
padding: 60px 0;
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%);
border-radius: 15px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.hero-title {
font-size: 3rem;
font-weight: bold;
color: #333333;
margin-bottom: 20px;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: none;
}
.hero-description {
font-size: 1.2rem;
color: #666666;
line-height: 1.6;
max-width: 600px;
margin: 0 auto;
}
.content-section {
background: rgba(255, 255, 255, 0.8);
padding: 40px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.content-section:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 2rem;
color: #333333;
margin-bottom: 15px;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-description {
font-size: 1.1rem;
color: #555555;
line-height: 1.6;
}
.footer {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: #ffffff;
padding: 40px 0 20px 0;
margin-top: 60px;
}
.footer-menu {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.footer-link {
color: #ffffff;
text-decoration: none;
font-weight: 500;
padding: 8px 12px;
border-radius: 5px;
transition: all 0.3s ease;
}
.footer-link:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.footer-disclaimer {
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
margin: 15px 0 10px 0;
font-style: italic;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
padding: 0 20px;
}
.footer p:last-child {
text-align: center;
color: rgba(255, 255, 255, 0.7);
font-size: 0.85rem;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 20px;
text-align: center;
}
.nav {
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
padding: 8px 12px;
font-size: 0.9rem;
}
.logo-text {
font-size: 1.5rem;
}
.logo-icon {
font-size: 2rem;
}
.hero-title {
font-size: 2.2rem;
}
.hero-description {
font-size: 1.1rem;
padding: 0 15px;
}
.content-section {
padding: 25px 20px;
margin-bottom: 20px;
}
.section-title {
font-size: 1.6rem;
}
.section-description {
font-size: 1rem;
}
.container {
padding: 20px 15px;
}
.hero-section {
padding: 40px 20px;
margin-bottom: 30px;
}
.footer-menu {
gap: 15px;
flex-direction: column;
align-items: center;
}
.footer-link {
font-size: 0.9rem;
padding: 6px 10px;
}
.footer-disclaimer {
font-size: 0.8rem;
margin: 12px 0 8px 0;
}
}
/* Extra small screens */
@media (max-width: 480px) {
.hero-title {
font-size: 1.8rem;
}
.nav {
gap: 10px;
}
.nav a {
padding: 6px 10px;
font-size: 0.85rem;
}
.language-switcher {
padding: 2px;
gap: 2px;
}
.language-button {
padding: 6px 10px;
font-size: 0.8rem;
min-width: 32px;
}
}
/* Global link styles */
a {
color: #667eea;
text-decoration: underline;
text-underline-offset: 0.2em;
text-decoration-thickness: 2px;
text-decoration-color: rgba(102, 126, 234, 0.4);
transition: color 0.2s ease, text-decoration-color 0.2s ease;
}
a:hover {
color: #764ba2;
text-decoration-color: #764ba2;
}
a:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
border-radius: 2px;
}
a:active {
opacity: 0.9;
}
/* Disabled state support */
a[aria-disabled="true"],
a.disabled {
opacity: 0.6;
pointer-events: none;
text-decoration-color: rgba(0, 0, 0, 0.2);
}