commit 72894b8907c9ed14daff456f9425e80dc2e67a6d Author: Claude Date: Fri Jul 31 11:17:32 2026 +0000 Scaffold Hugo + Decap trilingual site from WordPress migration plan Static replacement for the WordPress + Polylang + synchronous DeepL stack: - Hugo config: es (default) / de / pt-br, siblings paired by filename basename, WP slugs preserved via /:slug/ permalinks - Theme: CSS ported verbatim from the WP theme; header/footer/lang-switcher and article templates ported from their PHP counterparts (no Google Fonts — zero third-party requests) - seo-head partial: hreflang + x-default, og:locale(:alternate), JSON-LD BlogPosting/Blog with inLanguage (ported from theme/inc/seo.php) - robots.txt AI-crawler allowlist + build-time llms.txt (ported from theme/inc/robots-and-llms.php) - scripts/translate.py: async DeepL step (tag_handling=html, protected community terms, manual_translation freeze, fail-loud, bot commits) - scripts/wp-to-hugo.py: one-shot WP REST → Hugo content converter - Decap CMS admin with Gitea backend, locally-bundled JS - .woodpecker.yml: translate → build → deploy (rsync to Caddy root) - Sample trilingual welcome post + acerca/contacto pages Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017z2rT1oN7vMS4ggo23n5WG diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97a70eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Hugo build output +public/ +resources/ +.hugo_build.lock + +# Decap bundle is downloaded at build time (no third-party CDN at runtime) +static/admin/decap-cms.js + +__pycache__/ diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..7a7dcc7 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,45 @@ +# Woodpecker CI: translate → build → deploy +# +# Triggered by the Gitea push webhook. The translate step replaces the old +# synchronous WordPress save_post hook: publishing in Decap is one git +# commit, translation happens here, asynchronously, and every DeepL output +# lands as a reviewable bot commit. +# +# Secrets to configure in Woodpecker (repo settings → secrets): +# deepl_api_key — DeepL free-tier key (500k chars/mo) +# gitea_push_token — Gitea token for the translations bot user +# (translations@vienalatina.com) with repo write access +# +# The deploy step writes to the Caddy web root via a host mount, which +# requires the repo to be marked "trusted" in Woodpecker. + +when: + - event: push + branch: main + +steps: + translate: + image: python:3.12-slim + environment: + DEEPL_API_KEY: + from_secret: deepl_api_key + GITEA_PUSH_TOKEN: + from_secret: gitea_push_token + commands: + - pip install --quiet requests pyyaml + - python scripts/translate.py + + build: + image: hugomods/hugo:0.135.0 + commands: + # Bundle Decap locally so the /admin page makes zero third-party requests. + - wget -q -O static/admin/decap-cms.js https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js + - hugo --minify + + deploy: + image: alpine:3.20 + volumes: + - /var/www/vienalatina.com:/deploy-target + commands: + - apk add --no-cache rsync + - rsync -a --delete public/ /deploy-target/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..51e764e --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# Viena Latina + +Static, self-hosted, trilingual blog for the Latin American community in +Vienna — Hugo + Decap CMS + Gitea + Woodpecker CI on a single Hetzner CX22 +(Nuremberg, DE). Replaces the previous WordPress + Polylang + synchronous +DeepL stack. + +``` +Pablo ──► Decap CMS (/admin) ──commit──► Gitea ──webhook──► Woodpecker CI + │ + translate (DeepL, async) ─┤ + build (hugo) ─┤ + deploy (rsync) ─┘ + ▼ + Caddy 2 serves /var/www/vienalatina.com +``` + +## Languages + +Spanish is the authoring language; German (`/de/`) and Brazilian Portuguese +(`/pt-br/`) siblings are generated by CI as reviewable git commits. Siblings +pair by filename basename: + +``` +content/post/mi-articulo.es.md ← authored in Decap +content/post/mi-articulo.de.md ← written by scripts/translate.py +content/post/mi-articulo.pt-br.md ← written by scripts/translate.py +``` + +Set `manual_translation: true` in a sibling's frontmatter to freeze it — +CI will never overwrite it again. + +### Frontmatter contract + +```yaml +--- +title: "Mi artículo" +date: 2026-07-31 +lang: es +manual_translation: false # set true on *.de.md/*.pt-br.md to freeze +categories: [Gastronomía] +--- +``` + +## Translation flow + +1. Publish `*.es.md` in Decap → one git commit, no waiting on DeepL. +2. Gitea webhook fires Woodpecker. +3. `scripts/translate.py` diffs the push, translates changed `*.es.md` via + DeepL (`tag_handling=html`, protected community terms), writes the + siblings, and pushes them back as a bot commit + (`translations@vienalatina.com`). +4. `hugo --minify` builds, `rsync --delete` deploys, Caddy serves. + +Any DeepL error fails the pipeline visibly (red X, one-click retry) — +no half-translated sets ever ship. + +## Local development + +```sh +hugo server # http://localhost:1313 +``` + +## CI secrets (Woodpecker → repo settings → secrets) + +| Secret | Value | +|---|---| +| `deepl_api_key` | DeepL free-tier key (500k chars/mo) | +| `gitea_push_token` | Gitea token for the translations bot user, repo write access | + +The repo must be marked **trusted** in Woodpecker so the deploy step can +mount `/var/www/vienalatina.com`. + +## Decap CMS + +`static/admin/config.yml` uses the Gitea backend. Register an OAuth app in +Gitea admin (redirect URI `https://vienalatina.com/admin/`) and put its +client ID in `app_id`. The Decap JS bundle is downloaded at build time into +`static/admin/decap-cms.js` (gitignored) — zero third-party requests at +runtime. + +## GEO/SEO surfaces + +- `hreflang` + `og:locale(:alternate)` + JSON-LD `BlogPosting`/`Blog` with + `inLanguage` — `themes/vienalatina/layouts/partials/seo-head.html` +- `robots.txt` AI-crawler allowlist — `static/robots.txt` +- `llms.txt` — generated at build time from `layouts/index.llms.txt` +- `sitemap.xml` — Hugo native, multilingual + +## One-shot content migration + +```sh +pip install requests html2text +python scripts/wp-to-hugo.py https:// +``` + +Converts every WP post (with Polylang siblings) to +`content/post/..md`, downloads images into `static/uploads/`, +and preserves existing slugs. Spot-check ~10 articles before committing. +Add Caddy 301s for WP URL patterns that don't map cleanly +(`?p=123`, `/categoria/...` → `/categories/...`). diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..e180dac --- /dev/null +++ b/config.yaml @@ -0,0 +1,100 @@ +# Viena Latina — Hugo site configuration +# Three languages: Spanish (default, at /), German (/de/), Brazilian Portuguese (/pt-br/). +# Siblings pair by filename basename: mi-articulo.es.md ↔ mi-articulo.de.md ↔ mi-articulo.pt-br.md + +baseURL: "https://vienalatina.com/" +title: "Viena Latina" +theme: "vienalatina" + +defaultContentLanguage: "es" +defaultContentLanguageInSubdir: false + +# Preserve WordPress slugs so external inbound links keep working. +permalinks: + post: "/:slug/" + +taxonomies: + category: "categories" + tag: "tags" + +languages: + es: + languageCode: "es" + languageName: "Español" + weight: 1 + params: + description: "Comunidad latinoamericana en Viena — turismo, cultura, gastronomía, comunidad y comercio." + menus: + main: + - name: "Inicio" + url: "/" + weight: 1 + - name: "Acerca" + url: "/page/acerca/" + weight: 2 + - name: "Contacto" + url: "/page/contacto/" + weight: 3 + de: + languageCode: "de" + languageName: "Deutsch" + weight: 2 + params: + description: "Lateinamerikanische Community in Wien — Tourismus, Kultur, Gastronomie, Gemeinschaft und Handel." + menus: + main: + - name: "Start" + url: "/de/" + weight: 1 + - name: "Über uns" + url: "/de/page/acerca/" + weight: 2 + - name: "Kontakt" + url: "/de/page/contacto/" + weight: 3 + pt-br: + languageCode: "pt-BR" + languageName: "Português (Brasil)" + weight: 3 + params: + description: "Comunidade latino-americana em Viena — turismo, cultura, gastronomia, comunidade e comércio." + menus: + main: + - name: "Início" + url: "/pt-br/" + weight: 1 + - name: "Sobre" + url: "/pt-br/page/acerca/" + weight: 2 + - name: "Contato" + url: "/pt-br/page/contacto/" + weight: 3 + +params: + # og:locale values per language slug (ported from theme/inc/seo.php). + localeMap: + es: "es_ES" + de: "de_DE" + pt-br: "pt_BR" + +# llms.txt — custom plain-text output rendered from layouts/index.llms.txt +# (ported from theme/inc/robots-and-llms.php). +outputFormats: + llms: + mediaType: "text/plain" + baseName: "llms" + isPlainText: true + +outputs: + home: ["HTML", "RSS", "llms"] + +sitemap: + changefreq: "weekly" + priority: 0.5 + +markup: + goldmark: + renderer: + unsafe: true # WP-migrated content contains inline HTML + +enableRobotsTXT: false # robots.txt is a static file in static/ diff --git a/content/page/acerca.es.md b/content/page/acerca.es.md new file mode 100644 index 0000000..f35b4f6 --- /dev/null +++ b/content/page/acerca.es.md @@ -0,0 +1,9 @@ +--- +title: "Acerca" +lang: es +manual_translation: false +--- + +**Viena Latina** es la comunidad latinoamericana en Viena: guías de turismo, +agenda cultural, gastronomía, vida comunitaria y comercios latinos en la +ciudad. diff --git a/content/page/contacto.es.md b/content/page/contacto.es.md new file mode 100644 index 0000000..ba5ee2e --- /dev/null +++ b/content/page/contacto.es.md @@ -0,0 +1,7 @@ +--- +title: "Contacto" +lang: es +manual_translation: false +--- + +Escríbenos a **hola@vienalatina.com**. diff --git a/content/post/bienvenida-a-viena-latina.de.md b/content/post/bienvenida-a-viena-latina.de.md new file mode 100644 index 0000000..572acfa --- /dev/null +++ b/content/post/bienvenida-a-viena-latina.de.md @@ -0,0 +1,18 @@ +--- +title: "Willkommen bei Viena Latina" +date: 2026-07-31 +slug: bienvenida-a-viena-latina +lang: de +manual_translation: false +categories: [Comunidad] +description: "Die neue Website von Viena Latina: schneller, selbst gehostet und mit überprüfbaren Übersetzungen auf Deutsch und Portugiesisch." +--- + +Das ist das neue Zuhause von **Viena Latina**. Die Website ist jetzt komplett +statisch: Jeder Artikel wird auf Spanisch veröffentlicht und automatisch ins +Deutsche und brasilianische Portugiesisch übersetzt, mit eigenen URLs pro +Sprache. + +Für unsere Leserinnen und Leser ändert sich nichts — dieselben Rubriken wie +immer: Tourismus, Kultur, Gastronomie, Gemeinschaft und Handel. Alles lädt +schneller, ohne Cookies und ohne Tracker. diff --git a/content/post/bienvenida-a-viena-latina.es.md b/content/post/bienvenida-a-viena-latina.es.md new file mode 100644 index 0000000..5084ebe --- /dev/null +++ b/content/post/bienvenida-a-viena-latina.es.md @@ -0,0 +1,16 @@ +--- +title: "Bienvenida a Viena Latina" +date: 2026-07-31 +lang: es +manual_translation: false +categories: [Comunidad] +description: "El nuevo sitio de Viena Latina: más rápido, autoalojado y con traducciones revisables en alemán y portugués." +--- + +Este es el nuevo hogar de **Viena Latina**. El sitio ahora es completamente +estático: cada artículo se publica en español y se traduce automáticamente al +alemán y al portugués brasileño, con URLs propias para cada idioma. + +Nada cambia para quienes nos leen — las mismas secciones de siempre: Turismo, +Cultura, Gastronomía, Comunidad y Comercio. Todo carga más rápido, sin cookies +y sin rastreadores. diff --git a/content/post/bienvenida-a-viena-latina.pt-br.md b/content/post/bienvenida-a-viena-latina.pt-br.md new file mode 100644 index 0000000..695df2d --- /dev/null +++ b/content/post/bienvenida-a-viena-latina.pt-br.md @@ -0,0 +1,17 @@ +--- +title: "Bem-vindos ao Viena Latina" +date: 2026-07-31 +slug: bienvenida-a-viena-latina +lang: pt-br +manual_translation: false +categories: [Comunidad] +description: "O novo site do Viena Latina: mais rápido, auto-hospedado e com traduções revisáveis em alemão e português." +--- + +Este é o novo lar do **Viena Latina**. O site agora é totalmente estático: +cada artigo é publicado em espanhol e traduzido automaticamente para o alemão +e o português brasileiro, com URLs próprias para cada idioma. + +Nada muda para quem nos lê — as mesmas seções de sempre: Turismo, Cultura, +Gastronomia, Comunidade e Comércio. Tudo carrega mais rápido, sem cookies e +sem rastreadores. diff --git a/scripts/translate.py b/scripts/translate.py new file mode 100644 index 0000000..e2f771f --- /dev/null +++ b/scripts/translate.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Async DeepL translation step for Woodpecker CI. + +Replaces the synchronous `save_post` hook from the WordPress plugin +(plataforma_deepl_translate in plugin/plataforma-social/plataforma-social.php). + +For each *.es.md changed in the pushed commit range, calls DeepL with +tag_handling=html (markup survives translation) plus a glossary of +community-specific terms, and writes the *.de.md and *.pt-br.md siblings +next to the source. Sibling files carrying `manual_translation: true` in +their frontmatter are never overwritten. The siblings are committed back +to the same branch as a bot commit, then the pipeline builds and deploys. + +Failure behaviour: any DeepL error exits non-zero, the pipeline goes red, +and nothing partial is committed — no half-translated sets. + +Environment: + DEEPL_API_KEY required (Woodpecker secret) + DEEPL_API_URL optional, defaults to the free-tier endpoint + CI_COMMIT_SHA / CI_PREV_COMMIT_SHA provided by Woodpecker + +Dependencies: requests, pyyaml +""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +from pathlib import Path + +import requests +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEEPL_URL = os.environ.get("DEEPL_API_URL", "https://api-free.deepl.com/v2/translate") + +# Language slug -> DeepL target code (ported from the plugin's $lang_map). +TARGETS = { + "de": "DE", + "pt-br": "PT-BR", +} +SOURCE_SLUG = "es" +DEEPL_SOURCE = "ES" + +# Fixed community-specific terms DeepL must not "translate". +# Sent as ignored tags via tag_handling=html: each term is wrapped in +# … before the call and unwrapped after, which pins proper +# nouns without needing a server-side DeepL glossary resource. +PROTECTED_TERMS = [ + "Viena Latina", + "Grätzl", + "empanadas de viento", + "Naschmarkt", +] + +# Frontmatter keys whose string values get translated alongside the body. +TRANSLATED_KEYS = ("title", "description") + +BOT_NAME = "vienalatina-translations" +BOT_EMAIL = "translations@vienalatina.com" + + +def run(*args: str, check: bool = True) -> str: + result = subprocess.run(args, cwd=REPO_ROOT, check=check, capture_output=True, text=True) + return result.stdout.strip() + + +def changed_source_files() -> list[Path]: + """Spanish sources touched by the pushed commits. + + Filtering to *.es.md is what makes bot commits (which only add .de.md / + .pt-br.md) a no-op round — the re-fire loop the WP hook had to guard + against with meta flags cannot happen here. + """ + head = os.environ.get("CI_COMMIT_SHA", "HEAD") + prev = os.environ.get("CI_PREV_COMMIT_SHA", "") + if prev and not set(prev) <= {"0"}: + diff_range = [prev, head] + else: + diff_range = ["HEAD~1", "HEAD"] if run("git", "rev-list", "--count", "HEAD") != "1" else None + + if diff_range: + out = run("git", "diff", "--name-only", "--diff-filter=AM", *diff_range) + else: # very first commit in the repo: translate everything + out = run("git", "ls-files") + + files = [] + for line in out.splitlines(): + p = Path(line.strip()) + if p.suffix == ".md" and p.name.endswith(f".{SOURCE_SLUG}.md") and p.parts[:1] == ("content",): + full = REPO_ROOT / p + if full.exists(): + files.append(full) + return files + + +def split_frontmatter(text: str) -> tuple[dict, str]: + match = re.match(r"\A---\n(.*?)\n---\n?(.*)\Z", text, re.DOTALL) + if not match: + return {}, text + return yaml.safe_load(match.group(1)) or {}, match.group(2) + + +def join_frontmatter(fm: dict, body: str) -> str: + front = yaml.safe_dump(fm, allow_unicode=True, sort_keys=False, default_flow_style=False) + return f"---\n{front}---\n\n{body.lstrip()}" + + +def protect(text: str) -> str: + for term in PROTECTED_TERMS: + text = re.sub(re.escape(term), lambda m: f"{m.group(0)}", text, flags=re.IGNORECASE) + return text + + +def unprotect(text: str) -> str: + return re.sub(r"", "", text) + + +def deepl_translate(texts: list[str], target: str, html: bool) -> list[str]: + """Direct port of plataforma_deepl_translate(): same endpoint, same + tag_handling=html behaviour, but errors abort the pipeline instead of + silently shipping a half-translated post.""" + texts = [t for t in texts if isinstance(t, str) and t.strip()] + if not texts: + return [] + + key = os.environ.get("DEEPL_API_KEY", "") + if not key: + sys.exit("DEEPL_API_KEY is not set — configure the Woodpecker secret.") + + data: list[tuple[str, str]] = [ + ("target_lang", target), + ("source_lang", DEEPL_SOURCE), + ("tag_handling", "html"), + ("ignore_tags", "keep"), + ] + if not html: + # Titles/descriptions are plain strings; still use tag handling so + # protection works, DeepL just has no other tags to preserve. + pass + for t in texts: + data.append(("text", protect(t))) + + resp = requests.post( + DEEPL_URL, + headers={"Authorization": f"DeepL-Auth-Key {key}"}, + data=data, + timeout=60, + ) + if resp.status_code != 200: + sys.exit(f"DeepL HTTP {resp.status_code}: {resp.text[:300]}") + + return [unprotect(item["text"]) for item in resp.json().get("translations", [])] + + +def sibling_path(source: Path, slug: str) -> Path: + return source.with_name(source.name.replace(f".{SOURCE_SLUG}.md", f".{slug}.md")) + + +def is_frozen(path: Path) -> bool: + if not path.exists(): + return False + fm, _ = split_frontmatter(path.read_text(encoding="utf-8")) + return bool(fm.get("manual_translation")) + + +def translate_file(source: Path) -> list[Path]: + raw = source.read_text(encoding="utf-8") + fm, body = split_frontmatter(raw) + written = [] + + for slug, deepl_target in TARGETS.items(): + target_file = sibling_path(source, slug) + if is_frozen(target_file): + print(f" {target_file.relative_to(REPO_ROOT)}: manual_translation=true — skipped") + continue + + strings = [str(fm[k]) for k in TRANSLATED_KEYS if fm.get(k)] + translated_strings = deepl_translate(strings, deepl_target, html=False) + translated_body = deepl_translate([body], deepl_target, html=True) if body.strip() else [""] + + new_fm = dict(fm) + it = iter(translated_strings) + for k in TRANSLATED_KEYS: + if fm.get(k): + new_fm[k] = next(it) + new_fm["lang"] = slug + new_fm["manual_translation"] = False + # Pin the URL to the shared basename so it never drifts when a title + # is retranslated (plan: /de/mi-articulo/ pairs with /mi-articulo/). + new_fm.setdefault("slug", source.name.removesuffix(f".{SOURCE_SLUG}.md")) + + target_file.write_text(join_frontmatter(new_fm, translated_body[0] if translated_body else ""), encoding="utf-8") + written.append(target_file) + print(f" {target_file.relative_to(REPO_ROOT)}: written") + + return written + + +def commit_and_push(files: list[Path]) -> None: + branch = os.environ.get("CI_COMMIT_BRANCH", "main") + run("git", "config", "user.name", BOT_NAME) + run("git", "config", "user.email", BOT_EMAIL) + token = os.environ.get("GITEA_PUSH_TOKEN", "") + repo = os.environ.get("CI_REPO", "pablo/vienalatina") + if token: # Woodpecker's clone credentials are read-only; push needs its own token + run("git", "remote", "set-url", "origin", f"https://{BOT_NAME}:{token}@git.vienalatina.com/{repo}.git") + run("git", "add", *[str(f) for f in files]) + if not run("git", "status", "--porcelain"): + print("Translations identical to committed siblings — nothing to push.") + return + run("git", "commit", "-m", "translate: update DE and PT-BR siblings [skip-translate]") + run("git", "push", "origin", f"HEAD:{branch}") + print(f"Pushed sibling commit to {branch}.") + + +def main() -> None: + sources = changed_source_files() + if not sources: + print("No changed *.es.md files — nothing to translate.") + return + + all_written: list[Path] = [] + for source in sources: + print(f"Translating {source.relative_to(REPO_ROOT)}:") + all_written.extend(translate_file(source)) + + if all_written: + commit_and_push(all_written) + + +if __name__ == "__main__": + main() diff --git a/scripts/wp-to-hugo.py b/scripts/wp-to-hugo.py new file mode 100644 index 0000000..d56a57f --- /dev/null +++ b/scripts/wp-to-hugo.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""One-shot WordPress → Hugo content migration (disposable). + +Pulls every post from the WP REST API (with Polylang language codes), +converts HTML bodies to Markdown, downloads images into static/uploads/, +rewrites their URLs, and writes content/post/..md files whose +basenames pair Polylang siblings the way Hugo expects. + +Usage: + pip install requests html2text + python scripts/wp-to-hugo.py https://vielac.at + +Existing Spanish slugs are preserved so inbound links keep resolving after +the DNS cutover (add Caddy 301s for anything that doesn't map cleanly). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from urllib.parse import urlparse + +import html2text +import requests + +REPO_ROOT = Path(__file__).resolve().parent.parent +POSTS_DIR = REPO_ROOT / "content" / "post" +UPLOADS_DIR = REPO_ROOT / "static" / "uploads" + +# Polylang slug -> Hugo language slug used in filenames. +LANG_MAP = {"es": "es", "de": "de", "pt-br": "pt-br", "pt": "pt-br"} + + +def fetch_all(base_url: str, endpoint: str) -> list[dict]: + items, page = [], 1 + while True: + resp = requests.get( + f"{base_url}/wp-json/wp/v2/{endpoint}", + params={"per_page": 100, "page": page, "_embed": "1", "lang": ""}, + timeout=30, + ) + if resp.status_code == 400: # past the last page + break + resp.raise_for_status() + batch = resp.json() + if not batch: + break + items.extend(batch) + if page >= int(resp.headers.get("X-WP-TotalPages", 1)): + break + page += 1 + return items + + +def download_image(url: str) -> str: + """Fetch a WP upload into static/uploads/, return its new site-relative URL.""" + name = Path(urlparse(url).path).name + if not name: + return url + dest = UPLOADS_DIR / name + if not dest.exists(): + resp = requests.get(url, timeout=30) + if resp.status_code != 200: + print(f" ! image fetch failed ({resp.status_code}): {url}") + return url + UPLOADS_DIR.mkdir(parents=True, exist_ok=True) + dest.write_bytes(resp.content) + return f"/uploads/{name}" + + +def rewrite_images(html: str, base_url: str) -> str: + pattern = re.escape(base_url.rstrip("/")) + r"/wp-content/uploads/[^\s\"'<>)]+" + return re.sub(pattern, lambda m: download_image(m.group(0)), html) + + +def to_markdown(html: str) -> str: + conv = html2text.HTML2Text() + conv.body_width = 0 + conv.protect_links = True + return conv.handle(html).strip() + + +def frontmatter(post: dict, lang: str, categories: dict[int, str]) -> str: + title = to_markdown(post["title"]["rendered"]).replace('"', '\\"') + cats = [categories[c] for c in post.get("categories", []) if c in categories] + lines = [ + "---", + f'title: "{title}"', + f"date: {post['date']}", + f"slug: {post['slug']}", # preserve the exact WP slug per language + f"lang: {lang}", + "manual_translation: false", + f"categories: [{', '.join(cats)}]", + ] + excerpt = to_markdown(post.get("excerpt", {}).get("rendered", "")) + if excerpt: + lines.append(f'description: "{excerpt[:300].replace(chr(34), chr(39))}"') + embedded = post.get("_embedded", {}).get("wp:featuredmedia", []) + if embedded and embedded[0].get("source_url"): + lines.append(f"image: {download_image(embedded[0]['source_url'])}") + lines.append("---") + return "\n".join(lines) + + +def main() -> None: + if len(sys.argv) != 2: + sys.exit("Usage: python scripts/wp-to-hugo.py https://your-wp-site.example") + base_url = sys.argv[1].rstrip("/") + + categories = {c["id"]: c["name"] for c in fetch_all(base_url, "categories")} + posts = fetch_all(base_url, "posts") + print(f"Fetched {len(posts)} posts, {len(categories)} categories.") + + by_id = {p["id"]: p for p in posts} + POSTS_DIR.mkdir(parents=True, exist_ok=True) + written = 0 + + for post in posts: + lang = LANG_MAP.get(post.get("lang", "es")) + if not lang: + continue + + # Polylang exposes sibling IDs in `translations`; use the Spanish + # sibling's slug as the shared basename so Hugo pairs the set. + translations = post.get("translations", {}) + es_id = translations.get("es", post["id"]) + basename = by_id.get(es_id, post)["slug"] + + body_html = rewrite_images(post["content"]["rendered"], base_url) + md = frontmatter(post, lang, categories) + "\n\n" + to_markdown(body_html) + "\n" + + out = POSTS_DIR / f"{basename}.{lang}.md" + out.write_text(md, encoding="utf-8") + written += 1 + print(f" {out.relative_to(REPO_ROOT)}") + + print(f"Wrote {written} files. Spot-check ~10 before committing (formatting fidelity).") + + +if __name__ == "__main__": + main() diff --git a/static/admin/config.yml b/static/admin/config.yml new file mode 100644 index 0000000..95d0215 --- /dev/null +++ b/static/admin/config.yml @@ -0,0 +1,52 @@ +# Decap CMS configuration — Gitea backend (OAuth app registered in Gitea admin). +backend: + name: gitea + repo: pablo/vienalatina # owner/repo inside Gitea + branch: main + base_url: https://git.vienalatina.com + api_root: https://git.vienalatina.com/api/v1 + app_id: REPLACE_WITH_GITEA_OAUTH_CLIENT_ID + +site_url: https://vienalatina.com +display_url: https://vienalatina.com + +media_folder: static/uploads +public_folder: /uploads + +# Pablo authors Spanish sources only; CI generates the .de.md / .pt-br.md siblings. +collections: + - name: post + label: "Artículos" + label_singular: "Artículo" + folder: content/post + create: true + extension: md + format: yaml-frontmatter + slug: "{{slug}}.es" + filter: { field: lang, value: es } + fields: + - { name: title, label: "Título", widget: string } + - { name: date, label: "Fecha", widget: datetime } + - { name: lang, label: "Idioma", widget: hidden, default: es } + - { name: manual_translation, label: "Traducción manual (congelar)", widget: boolean, default: false, required: false } + - { name: categories, label: "Categorías", widget: select, multiple: true, + options: ["Turismo", "Cultura", "Gastronomía", "Comunidad", "Comercio"] } + - { name: description, label: "Resumen", widget: text, required: false } + - { name: image, label: "Imagen destacada", widget: image, required: false } + - { name: body, label: "Cuerpo", widget: markdown } + + - name: page + label: "Páginas" + label_singular: "Página" + folder: content/page + create: true + extension: md + format: yaml-frontmatter + slug: "{{slug}}.es" + filter: { field: lang, value: es } + fields: + - { name: title, label: "Título", widget: string } + - { name: date, label: "Fecha", widget: datetime, required: false } + - { name: lang, label: "Idioma", widget: hidden, default: es } + - { name: manual_translation, label: "Traducción manual (congelar)", widget: boolean, default: false, required: false } + - { name: body, label: "Cuerpo", widget: markdown } diff --git a/static/admin/index.html b/static/admin/index.html new file mode 100644 index 0000000..8beaa36 --- /dev/null +++ b/static/admin/index.html @@ -0,0 +1,14 @@ + + + + + + + Viena Latina — Admin + + + + + + diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..fa90123 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,39 @@ +# Viena Latina — robots.txt +# Ported from theme/inc/robots-and-llms.php (PR #37): explicit allow for AI crawlers. + +User-agent: * +Allow: / +Disallow: /admin/ + +# AI crawlers — explicit allow +User-agent: GPTBot +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: Amazonbot +Allow: / + +User-agent: Bytespider +Allow: / + +User-agent: Applebot-Extended +Allow: / + +User-agent: meta-externalagent +Allow: / + +Sitemap: https://vienalatina.com/sitemap.xml diff --git a/static/uploads/.gitkeep b/static/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/themes/vienalatina/assets/css/main.css b/themes/vienalatina/assets/css/main.css new file mode 100644 index 0000000..2379cda --- /dev/null +++ b/themes/vienalatina/assets/css/main.css @@ -0,0 +1,3705 @@ +/* ========================================================= + Plataforma — Main Stylesheet (v1.2) + Layout: sticky platform header + full-width feed. + ========================================================= */ + +/* Design tokens — duplicated here so main.css is self-contained + regardless of whether style.css is also enqueued. */ +:root { + color-scheme: light; + + --brand: #c0391c; + --brand-dark: #9a2a0f; + --brand-soft: #fbe2db; + --brand-softer: #fdf1ed; + + --bg: #f8f3ef; + --surface: #ffffff; + --surface-soft: #f2ebe6; + --border: #c2a89f; + --border-light: #e0cec8; + + --text: #1a0d09; + --muted: #5c3d37; + --subtle: #9a7870; + + --shadow: 0 2px 16px rgba(60, 20, 10, 0.09); + --shadow-sm: 0 1px 6px rgba(60, 20, 10, 0.06); + --shadow-lg: 0 8px 32px rgba(60, 20, 10, 0.13); + --radius-xl: 18px; + --radius-lg: 12px; + --radius-md: 8px; + --radius-sm: 5px; + + --accent: var(--brand); + --accent-soft: var(--brand-soft); + --panel: var(--surface); + --panel-border: var(--border); +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + min-height: 100vh; + font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-weight: 400; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, h2, h3, h4 { + font-weight: 700; + letter-spacing: -0.01em; +} + +button, input, select, textarea { font: inherit; } + +button { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +a { color: var(--brand); text-decoration: none; } +a:hover { text-decoration: underline; } + +img { max-width: 100%; height: auto; } + +/* ========================================================= + Sticky platform header + ========================================================= */ + +.site-bar { + position: sticky; + top: 0; + z-index: 50; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border, #c2a89f); +} + +.site-bar__inner { + width: min(960px, calc(100% - 32px)); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 0; + min-height: 62px; +} + +/* WP outputs — match both cases */ +.site-bar__brand, +.site-bar .custom-logo-link { + display: inline-flex; + align-items: center; + flex-shrink: 0; + line-height: 0; + text-decoration: none; + color: var(--text); +} + +.site-bar__brand:hover, +.site-bar .custom-logo-link:hover { text-decoration: none; } + +.site-bar__brand img, +.site-bar .custom-logo-link img, +.site-bar img.custom-logo { + display: block; + height: auto; + width: auto; + max-height: 32px; + max-width: 180px; + object-fit: contain; +} + +.site-bar__brand-text { + font-size: 1.15rem; + font-weight: 700; + color: var(--brand); + letter-spacing: -0.02em; +} + +.site-bar__nav { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.92rem; + margin-left: auto; +} + +/* Language switcher — ES · DE · PT */ +.lang-switcher { + display: flex; + align-items: center; + gap: 2px; + margin-right: 4px; + padding-right: 10px; + border-right: 1px solid var(--border-light, #e0cec8); +} + +.lang-btn { + background: none; + border: none; + padding: 4px 5px; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.05em; + color: var(--subtle, #9a7870); + cursor: pointer; + border-radius: var(--radius-sm, 5px); + transition: color 120ms ease, background 120ms ease; + line-height: 1; + text-decoration: none; +} + +.lang-btn[aria-current="true"] { + color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); +} + +.lang-btn:hover { color: var(--text, #1a0d09); } + +.lang-btn[aria-pressed="true"] { + color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); +} + +.lang-sep { + color: var(--border, #c2a89f); + font-size: 0.72rem; + line-height: 1; + pointer-events: none; + user-select: none; +} + +/* Mobile: centred inside hamburger panel */ +.site-bar__mobile-account .lang-switcher { + border-right: none; + padding-right: 0; + margin-right: 0; + margin-bottom: 8px; + justify-content: center; +} + +/* Plain nav links — Tablero, etc. */ +.site-bar__link { + color: var(--muted); + font-weight: 600; + padding: 6px 10px; + border-radius: 999px; + transition: background 140ms ease, color 140ms ease; + white-space: nowrap; +} + +.site-bar__link:hover { + background: var(--brand-softer); + color: var(--brand); + text-decoration: none; +} + +/* Accent link (Escribir) */ +.site-bar__link--accent { + color: white; + background: var(--brand); +} + +.site-bar__link--accent:hover { + background: var(--brand-dark); + color: white; +} + +.site-bar__write { + display: inline-flex; + align-items: center; + gap: 5px; +} + +/* CTA pill buttons — Ingresar / Salir */ +.site-bar__btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 20px; + border-radius: 999px; + font-weight: 700; + font-size: 0.9rem; + white-space: nowrap; + text-decoration: none; + transition: background 140ms ease, color 140ms ease, + border-color 140ms ease, box-shadow 140ms ease, + transform 80ms ease; +} + +.site-bar__btn:hover { text-decoration: none; } + +.site-bar__btn--primary { + background: var(--brand); + color: #fff; + border: 2px solid var(--brand); + box-shadow: 0 2px 8px rgba(192, 57, 28, 0.22); +} + +.site-bar__btn--primary:hover { + background: var(--brand-dark); + border-color: var(--brand-dark); + color: #fff; + transform: translateY(-1px); + box-shadow: 0 4px 14px rgba(192, 57, 28, 0.32); +} + +.site-bar__btn--primary:active { + transform: translateY(0); + box-shadow: none; +} + +.site-bar__btn--ghost { + background: transparent; + color: var(--muted, #5c3d37); + border: 1.5px solid var(--muted, #5c3d37); +} + +.site-bar__btn--ghost:hover { + border-color: var(--brand, #c0391c); + color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); +} + +/* Logged-in user label — "Conectado como Name" */ +.site-bar__user { + font-size: 0.82rem; + color: var(--subtle, #9a7870); + font-weight: 400; + white-space: nowrap; + padding-right: 4px; + border-right: 1px solid var(--border-light, #e0cec8); +} +.site-bar__user strong { + font-weight: 700; + color: var(--muted, #5c3d37); +} + +/* Hamburger — hidden on desktop, visible on mobile */ +.site-bar__hamburger { + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 5px; + width: 42px; + height: 42px; + padding: 0; + margin-left: 10px; + background: var(--brand, #c0391c); + border: none; + border-radius: var(--radius-md, 8px); + cursor: pointer; + transition: background 140ms ease; + flex-shrink: 0; +} + +.site-bar__hamburger:hover { + background: var(--brand-dark, #9a2a0f); +} + +.hamburger-line { + display: block; + width: 20px; + height: 2.5px; + background: #fff; + border-radius: 2px; + transition: transform 200ms ease, opacity 200ms ease; +} + +/* Animated X when open */ +.site-bar__hamburger[aria-expanded="true"] .hamburger-line:nth-child(1) { + transform: translateY(7.5px) rotate(45deg); +} +.site-bar__hamburger[aria-expanded="true"] .hamburger-line:nth-child(2) { + opacity: 0; +} +.site-bar__hamburger[aria-expanded="true"] .hamburger-line:nth-child(3) { + transform: translateY(-7.5px) rotate(-45deg); +} + +@media (max-width: 520px) { + .site-bar__write-label { display: none; } + .site-bar__user { display: none; } + /* "Mi Espacio" moved into hamburger on mobile — hidden from top bar */ + .site-bar__link[href*="tablero"] { display: none; } +} + +/* Account links inside hamburger nav — desktop: hidden, mobile: shown */ +.site-bar__mobile-account { + display: none; +} +@media (max-width: 768px) { + .site-bar__mobile-account { + display: flex; + flex-direction: column; + margin: 6px 16px 4px; + border-top: 1px solid var(--border-light, #e0cec8); + padding-top: 6px; + } + .site-bar__mobile-account-link { + display: flex; + align-items: center; + gap: 10px; + padding: 11px 6px; + font-size: 0.95rem; + font-weight: 600; + color: var(--muted, #5c3d37); + text-decoration: none; + border-bottom: 1px solid var(--border-light, #e0cec8); + transition: color 120ms ease; + } + .site-bar__mobile-account-link:last-child { border-bottom: 0; } + .site-bar__mobile-account-link:hover { color: var(--text, #1a0d09); } + .site-bar__mobile-account-link--primary { color: var(--brand, #c0391c); } + .site-bar__mobile-account-link--out { color: var(--subtle, #9a7870); } +} + +@media (max-width: 768px) { + .site-bar__hamburger { display: flex; } +} + +/* ========================================================= + Primary page navigation — tab strip below auth bar + ========================================================= */ + +.site-bar__pages { + border-top: 1px solid var(--border-light, #e0cec8); + background: var(--surface, #fff); +} + +/* On mobile/tablet the nav is collapsed and toggled by the hamburger */ +@media (max-width: 768px) { + .site-bar__pages { + display: none; + flex-direction: column; + padding: 8px 0 12px; + border-top: 2px solid var(--brand-soft, #fbe2db); + box-shadow: 0 4px 16px rgba(60, 20, 10, 0.08); + } + + .site-bar__pages.is-open { + display: flex; + } + + .page-nav { + flex-direction: column; + padding: 0; + overflow: visible; + } + + .page-nav li a { + padding: 11px 22px; + font-size: 0.95rem; + border-bottom: none; + border-left: 3px solid transparent; + } + + .page-nav li a:hover, + .page-nav li.current-menu-item a, + .page-nav li.current_page_item a { + border-left-color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); + } + + /* Sub-menu items shown inline on mobile (no hover dropdown) */ + .page-nav .sub-menu { + position: static; + display: block; + box-shadow: none; + border: none; + background: transparent; + padding: 0; + opacity: 1; + pointer-events: auto; + visibility: visible; + } + + .page-nav .sub-menu li a { + padding-left: 38px; + font-size: 0.87rem; + color: var(--muted, #5c3d37); + } + + .page-nav .menu-item-has-children > a::after { + display: none; + } + + .home .page-nav .page-nav__home a, + .blog .page-nav .page-nav__home a { + border-left-color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); + border-bottom: none; + } +} + +.page-nav { + display: flex; + list-style: none; + margin: 0 auto; + padding: 0 20px; + max-width: 960px; + overflow: visible; /* must be visible so sub-menu dropdowns aren't clipped */ + scrollbar-width: none; + -webkit-overflow-scrolling: touch; +} + +.page-nav::-webkit-scrollbar { display: none; } + +.page-nav li { + flex-shrink: 0; + position: relative; /* anchor for sub-menu */ +} + +.page-nav li a { + display: block; + padding: 10px 16px; + font-size: 0.88rem; + font-weight: 600; + color: var(--muted, #5c3d37); + text-decoration: none; + border-bottom: 2px solid transparent; + transition: color 130ms ease, border-color 130ms ease; + white-space: nowrap; +} + +.page-nav li a:hover { + color: var(--text, #1a0d09); + text-decoration: none; +} + +.page-nav li.current-menu-item a, +.page-nav li.current_page_item a, +.page-nav li.current-menu-ancestor a { + color: var(--brand, #c0391c); + border-bottom-color: var(--brand, #c0391c); +} + +.home .page-nav .page-nav__home a, +.blog .page-nav .page-nav__home a { + color: var(--brand, #c0391c); + border-bottom-color: var(--brand, #c0391c); +} + +/* Dropdown indicator for items that have children */ +.page-nav .menu-item-has-children > a::after { + content: ' ▾'; + font-size: 0.68em; + opacity: 0.65; + letter-spacing: 0; +} + +/* Sub-menu dropdown */ +.page-nav .sub-menu { + display: none; + position: absolute; + top: calc(100% + 1px); + left: 0; + min-width: 190px; + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-lg, 12px); + box-shadow: var(--shadow, 0 2px 16px rgba(60, 20, 10, 0.09)); + padding: 6px 0; + z-index: 300; + list-style: none; + margin: 0; +} + +.page-nav li:hover > .sub-menu, +.page-nav li:focus-within > .sub-menu { + display: block; +} + +.page-nav .sub-menu li a { + padding: 10px 20px; + border-bottom: none; + font-size: 0.87rem; + font-weight: 500; + color: var(--muted, #5c3d37); + white-space: nowrap; +} + +.page-nav .sub-menu li a:hover { + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); + border-bottom: none; +} + +/* First/last sub-menu items get rounded corners to match the container */ +.page-nav .sub-menu li:first-child a { border-radius: 10px 10px 0 0; } +.page-nav .sub-menu li:last-child a { border-radius: 0 0 10px 10px; } +.page-nav .sub-menu li:only-child a { border-radius: 10px; } + +@media (max-width: 600px) { + .page-nav li a { font-size: 0.82rem; padding: 9px 12px; } + /* On mobile the nav scrolls horizontally — hide dropdowns to avoid clipping */ + .page-nav .sub-menu { display: none !important; } +} + +/* ── Category badges and group pills as links ────────────────────────────── */ + +a.article-card__kind { + text-decoration: none; + cursor: pointer; +} + +a.article-card__kind:hover { + background: var(--brand, #c0391c); + color: #fff; +} + +a.group-pill { + text-decoration: none; + cursor: pointer; + transition: background 130ms ease, color 130ms ease; +} + +a.group-pill:hover { + background: var(--brand, #c0391c); + color: #fff; +} + +/* ========================================================= + /escribir/ — full-page editor + ========================================================= */ + +.escribir-page__title { + font-size: 1.6rem; + margin-bottom: 20px; + padding-bottom: 14px; + border-bottom: 1px solid var(--border); +} + +.escribir-page .compose-editor { + min-height: 260px; +} + +.escribir-page__actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + margin-top: 8px; +} + +/* ========================================================= + Page shell (centered column) + ========================================================= */ + +.page-shell { + width: min(680px, calc(100% - 40px)); + margin: 0 auto; + padding: 24px 0 56px; +} + +/* ========================================================= + Feed layout (single column, full width) + ========================================================= */ + +.feed { + display: flex; + flex-direction: column; + gap: 18px; +} + +.feed--single { + gap: 0; +} + +/* ========================================================= + Composer bar (Facebook-style) + ========================================================= */ + +.composer-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + background: var(--surface, #fff); + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-xl, 18px); + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60, 20, 10, 0.06)); +} + +.composer-bar__input-trigger { + flex: 1; + display: flex; + align-items: center; + gap: 12px; + padding: 11px 18px; + border: 0; + border-radius: 999px; + background: var(--surface-soft, #f2ebe6); + color: var(--muted, #5c3d37); + cursor: pointer; + text-align: left; + font-size: 1rem; + transition: background 140ms ease; +} + +.composer-bar__input-trigger:hover { + background: var(--brand-softer); +} + +.composer-bar__avatar { + display: inline-flex; + flex-shrink: 0; +} + +.composer-bar__avatar-img, +.composer-bar__avatar img { + border-radius: 50%; + display: block; +} + +.composer-bar__placeholder { + flex: 1; + font-style: italic; +} + +.composer-bar__event-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 16px; + border: 0; + border-radius: 999px; + background: var(--brand); + color: white; + font-weight: 700; + cursor: pointer; + transition: background 140ms ease, transform 120ms ease; + white-space: nowrap; +} + +.composer-bar__event-btn:hover { + background: var(--brand-dark); + transform: translateY(-1px); +} + +.composer-bar__event-btn .cal-icon { flex-shrink: 0; } + +@media (max-width: 600px) { + .composer-bar__event-btn span:not(.composer-bar__event-icon) { + display: none; + } + .composer-bar__event-btn { padding: 10px 12px; } +} + +/* ========================================================= + Inline composer panel (expands below the bar, no overlay) + ========================================================= */ + +.composer-panel { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 280ms cubic-bezier(.4, 0, .2, 1), + margin-top 280ms cubic-bezier(.4, 0, .2, 1); + margin-top: 0; +} + +.composer-panel.is-open { + grid-template-rows: 1fr; + margin-top: 12px; +} + +.composer-panel__inner { + overflow: hidden; + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-xl, 18px); + background: var(--surface, #fff); + box-shadow: var(--shadow, 0 2px 16px rgba(60,20,10,0.09)); + color: var(--text, #1a0d09); + /* padding transitions from 0 → full when open */ + padding: 0 26px; + transition: padding 280ms cubic-bezier(.4, 0, .2, 1); +} + +.composer-panel.is-open .composer-panel__inner { + padding: 24px 26px 20px; +} + +.composer-panel__head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-light, #e0cec8); +} + +.composer-panel__title { + margin: 0; + font-size: 1.15rem; + font-weight: 700; + color: var(--text, #1a0d09); +} + +.composer-panel__close { + width: 32px; + height: 32px; + border: 0; + border-radius: 50%; + background: transparent; + color: var(--muted, #5c3d37); + font-size: 1.6rem; + line-height: 1; + cursor: pointer; + transition: background 120ms ease, color 120ms ease; +} + +.composer-panel__close:hover { + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); +} + +.composer-panel__actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--border-light, #e0cec8); +} + +/* ========================================================= + Form + ========================================================= */ + +/* Category multi-select chips */ +.cat-picker { + border: 0; + padding: 0; + margin: 0; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-start; +} +.cat-picker legend { + font-weight: 600; + font-size: 0.95rem; + color: var(--text, #1a0d09); + margin-bottom: 10px; + width: 100%; + float: left; +} +.cat-picker__item { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + background: var(--surface-soft, #f2ebe6); + border: 1.5px solid var(--border-light, #e0cec8); + border-radius: 999px; + cursor: pointer; + font-weight: 500; + font-size: 0.88rem; + color: var(--muted, #5c3d37); + transition: background 120ms ease, border-color 120ms ease, color 120ms ease; + user-select: none; +} +.cat-picker__item:hover { + background: var(--brand-softer, #fdf1ed); + border-color: var(--brand-soft, #fbe2db); + color: var(--brand, #c0391c); +} +.cat-picker__item:has(input:checked) { + background: var(--brand, #c0391c); + color: #fff; + border-color: var(--brand, #c0391c); +} +.cat-picker__item input[type="checkbox"] { + display: none; +} + +.article-form { + display: grid; + gap: 18px; +} + +.article-form label { + display: grid; + gap: 8px; + font-weight: 600; + font-size: 0.95rem; + color: var(--text); +} + +.article-form input, +.article-form select, +.article-form textarea { + width: 100%; + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + padding: 12px 14px; + background: var(--surface-soft, #f2ebe6); + color: var(--text, #1a0d09); + font-weight: 400; + transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; +} + +.article-form input:focus, +.article-form select:focus, +.article-form textarea:focus { + outline: 0; + border-color: var(--brand, #c0391c); + background: #fff; + box-shadow: 0 0 0 3px rgba(192, 57, 28, 0.14); +} + +.article-form textarea { resize: vertical; } + +.char-counter { + font-size: 0.75rem; + color: var(--subtle); + text-align: right; + font-weight: 400; +} + +.char-counter.is-near-limit { color: var(--brand); } + +/* Buttons */ +.btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + line-height: 1; + padding: 12px 26px; + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.01em; + color: #fff; + background: var(--brand, #c0391c); + border: 0; + border-radius: 999px; + cursor: pointer; + box-shadow: 0 2px 10px rgba(192, 57, 28, 0.20); + transition: background 140ms ease, transform 100ms ease, box-shadow 140ms ease; +} + +.btn-primary:hover { + background: var(--brand-dark, #9a2a0f); + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(192, 57, 28, 0.28); +} + +.btn-primary:disabled { + background: var(--subtle, #9a7870); + box-shadow: none; + cursor: default; + transform: none; +} + +.btn-ghost { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + line-height: 1; + padding: 11px 22px; + font-size: 0.95rem; + font-weight: 600; + color: var(--muted, #5c3d37); + background: transparent; + border: 1.5px solid var(--border, #c2a89f); + border-radius: 999px; + cursor: pointer; + transition: background 140ms ease, border-color 140ms ease, color 140ms ease; +} + +.btn-ghost:hover { + background: var(--surface-soft, #f2ebe6); + border-color: var(--muted, #5c3d37); + color: var(--text, #1a0d09); +} + +/* Notice */ +.notice { + padding: 11px 14px; + border-radius: var(--radius-md); + font-size: 0.92rem; + margin-bottom: 12px; +} + +.notice--success { + background: #e7f7ed; + color: #1a5c32; + border: 1px solid #b8dec5; +} + +.notice--error { + background: var(--brand-softer); + color: var(--brand-dark); + border: 1px solid var(--brand-soft); +} + +/* ========================================================= + Wall + filters + ========================================================= */ + +.wall { + display: flex; + flex-direction: column; + gap: 14px; +} + +.wall__filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 4px 2px; +} + +.filter-chip { + padding: 8px 16px; + min-height: 36px; + border: 1.5px solid var(--border-light, #e0cec8); + border-radius: 999px; + background: var(--surface-soft, #f2ebe6); + color: var(--muted, #5c3d37); + font-size: 0.87rem; + font-weight: 600; + cursor: pointer; + transition: background 130ms ease, color 130ms ease, border-color 130ms ease; +} + +.filter-chip:hover { + background: var(--brand-softer, #fdf1ed); + border-color: var(--brand-soft, #fbe2db); + color: var(--brand, #c0391c); +} + +.filter-chip.is-active { + background: var(--brand, #c0391c); + color: #fff; + border-color: var(--brand, #c0391c); + box-shadow: 0 2px 8px rgba(192, 57, 28, 0.20); +} + +.articles { + display: flex; + flex-direction: column; + gap: 14px; +} + +/* ========================================================= + Article card + ========================================================= */ + +.article-card { + padding: 22px 24px; + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-xl, 18px); + background: var(--surface, #fff); + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60, 20, 10, 0.06)); + transition: box-shadow 160ms ease, transform 160ms ease; + overflow: hidden; /* clip cover image to rounded corners */ +} + +/* Remove top padding when the card opens with a cover image */ +.article-card--has-cover { + padding-top: 0; +} + +/* Cover image bleeds to the left/right edges and top of the card */ +.article-card__cover-link { + display: block; + margin: 0 -24px 16px; +} + +.article-card__cover { + display: block; + width: 100%; + height: 220px; + object-fit: cover; + /* border-radius top-corners match the card; overflow:hidden on .article-card clips it */ +} + +.article-card:hover { + box-shadow: var(--shadow, 0 2px 16px rgba(60, 20, 10, 0.09)); + transform: translateY(-2px); +} + +.article-card.is-hidden { display: none; } + +.article-card__head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.article-card__avatar { + border-radius: 50%; + flex-shrink: 0; +} + +.article-card__byline { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.article-card__author { + font-weight: 700; + color: var(--text); + font-size: 0.96rem; +} + +.article-card__author:hover { + color: var(--brand); + text-decoration: none; +} + +.article-card__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + color: var(--muted); + font-size: 0.82rem; +} + +.article-card__kind { + display: inline-flex; + padding: 3px 10px; + border-radius: 999px; + background: var(--brand-soft, #fbe2db); + color: var(--brand, #c0391c); + font-size: 0.70rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.article-card__title { + margin: 0 0 10px; + font-size: 1.35rem; + line-height: 1.25; +} + +.article-card__title a { + color: var(--text); +} + +.article-card__title a:hover { + color: var(--brand); + text-decoration: none; +} + +.article-card__excerpt { + margin: 0; + line-height: 1.55; + color: var(--muted); + font-size: 0.98rem; +} + +.article-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 18px; + padding-top: 14px; + border-top: 1px solid var(--border-light, #e0cec8); +} + +.article-card__read-more { + font-size: 0.88rem; + font-weight: 600; + color: var(--brand); +} + +/* ========================================================= + Like button + ========================================================= */ + +.like-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1.5px solid var(--border, #c2a89f); + border-radius: 999px; + background: transparent; + color: var(--muted, #5c3d37); + font-size: 0.88rem; + cursor: pointer; + transition: all 120ms ease; +} + +.like-btn:hover { + border-color: var(--brand); + color: var(--brand); + background: var(--brand-softer); +} + +.like-btn.is-liked { + background: var(--brand-soft); + border-color: var(--brand); + color: var(--brand); + font-weight: 700; +} + +.like-btn .like-heart { font-size: 1rem; line-height: 1; } + +.like-btn[data-pending="true"] { opacity: 0.7; cursor: wait; } + +/* ========================================================= + Share — compact (dropdown in post cards) + ========================================================= */ + +.share-dropdown { + position: relative; + display: inline-block; +} + +.share-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1.5px solid var(--border, #c2a89f); + border-radius: 999px; + background: transparent; + color: var(--muted, #5c3d37); + font-size: 0.88rem; + font-family: inherit; + cursor: pointer; + transition: all 120ms ease; +} + +.share-btn:hover { + border-color: var(--brand); + color: var(--brand); + background: var(--brand-softer); +} + +.share-btn[aria-expanded="true"] { + border-color: var(--brand); + color: var(--brand); + background: var(--brand-soft); +} + +.share-dropdown__menu { + position: absolute; + bottom: calc(100% + 8px); + right: 0; + z-index: 30; + display: flex; + flex-direction: column; + min-width: 180px; + padding: 6px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); +} + +.share-dropdown__menu[hidden] { display: none; } + +.share-dropdown__item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: var(--text); + font-family: inherit; + font-size: 0.9rem; + text-align: left; + text-decoration: none; + cursor: pointer; + width: 100%; + transition: background 120ms ease, color 120ms ease; +} + +.share-dropdown__item:hover { + background: var(--brand-softer); + color: var(--brand); + text-decoration: none; +} + +.share-dropdown__item svg { flex-shrink: 0; } + +.share-dropdown__item--whatsapp:hover { color: #25D366; } +.share-dropdown__item--facebook:hover { color: #1877F2; } +.share-dropdown__item--twitter:hover { color: #000; } +.share-dropdown__item--linkedin:hover { color: #0A66C2; } +.share-dropdown__item--email:hover { color: var(--brand); } + +/* Copied confirmation */ +.share-dropdown__item--copy.is-copied, +.share-pill--copy.is-copied { + background: #1a5c32; + color: white; + border-color: #1a5c32; +} + +/* ========================================================= + Share — full (inline pill row on single pages) + ========================================================= */ + +.share-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin: 22px 0; + padding: 16px 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.share-row__label { + font-size: 0.88rem; + color: var(--muted); + font-weight: 600; + margin-right: 4px; +} + +.share-pill { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + border: 1.5px solid var(--border); + border-radius: 50%; + background: var(--surface); + color: var(--muted); + cursor: pointer; + transition: all 140ms ease; + text-decoration: none; + font-family: inherit; +} + +.share-pill:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-sm); + text-decoration: none; +} + +.share-pill--whatsapp:hover { background: #25D366; color: white; border-color: #25D366; } +.share-pill--facebook:hover { background: #1877F2; color: white; border-color: #1877F2; } +.share-pill--twitter:hover { background: #000; color: white; border-color: #000; } +.share-pill--linkedin:hover { background: #0A66C2; color: white; border-color: #0A66C2; } +.share-pill--telegram:hover { background: #229ED9; color: white; border-color: #229ED9; } +.share-pill--email:hover { background: var(--brand); color: white; border-color: var(--brand); } +.share-pill--copy:hover { background: var(--brand); color: white; border-color: var(--brand); } + +@media (max-width: 600px) { + .share-pill { width: 36px; height: 36px; } +} + +/* ========================================================= + Article card footer — adjust for share button + ========================================================= */ + +.article-card__footer { + flex-wrap: wrap; + gap: 8px; +} + +/* ========================================================= + Single post view + ========================================================= */ + +.article-single { + padding: 26px 28px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-sm); +} + +.article-single__head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.article-single__title { + margin: 8px 0 14px; + font-size: clamp(1.7rem, 4vw, 2.3rem); + line-height: 1.15; +} + +.article-single__summary { + font-size: 1.1rem; + color: var(--muted); + line-height: 1.6; + margin: 0 0 20px; + padding: 0 0 18px; + border-bottom: 1px solid var(--border); +} + +.article-single__body { + line-height: 1.7; + font-size: 1.02rem; +} + +.article-single__body p { margin: 0 0 1.1em; } + +.like-section { + margin: 22px 0 16px; + padding: 16px 0 0; + border-top: 1px solid var(--border); +} + +.back-link { + display: inline-block; + margin-top: 8px; + font-size: 0.92rem; + color: var(--muted); +} + +.back-link:hover { color: var(--brand); } + +/* ========================================================= + Empty state + ========================================================= */ + +.empty-state { + padding: 32px 18px; + text-align: center; + color: var(--muted); + border: 1px dashed var(--border); + border-radius: var(--radius-xl); + background: var(--surface); +} + +/* ========================================================= + Footer + ========================================================= */ + +.site-foot { + margin-top: 36px; + padding: 18px 0; + border-top: 1px solid var(--border); + text-align: center; + font-size: 0.85rem; + color: var(--subtle); +} + +/* ========================================================= + WordPress core image alignment + ========================================================= */ + +.alignleft { float: left; margin: 0 1.5em 1em 0; } +.alignright { float: right; margin: 0 0 1em 1.5em; } +.aligncenter { display: block; margin: 0 auto 1em; } +.wp-caption { max-width: 100%; } +.wp-caption-text { font-size: 0.85rem; color: var(--muted); } + +/* ========================================================= + Clickable avatar wrapper (post cards + single post) + ========================================================= */ + +.article-card__avatar-link { + display: inline-flex; + flex-shrink: 0; + border-radius: 50%; + line-height: 0; + transition: opacity 120ms ease; +} + +.article-card__avatar-link:hover { opacity: 0.8; } + +/* ========================================================= + Author profile page (/author/username/) + ========================================================= */ + +.author-profile { + display: flex; + align-items: flex-start; + gap: 18px; + padding: 22px 24px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-sm); +} + +.author-profile__avatar-link { + display: block; + flex-shrink: 0; + border-radius: 50%; + line-height: 0; +} + +.author-profile__avatar { + width: 88px; + height: 88px; + border-radius: 50%; + display: block; + object-fit: cover; + border: 3px solid var(--brand-soft); +} + +.author-profile__body { + flex: 1; + min-width: 0; + padding-top: 4px; +} + +.author-profile__name { + margin: 0 0 6px; + font-size: 1.5rem; + line-height: 1.2; + color: var(--text); +} + +.author-profile__bio { + margin: 0 0 8px; + font-size: 0.97rem; + color: var(--muted); + line-height: 1.55; +} + +.author-profile__groups { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 6px 0 4px; +} + +.author-profile__group-box { + margin-top: 12px; + padding: 12px 16px; + background: var(--surface-soft, #f2ebe6); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-lg, 12px); +} +.author-profile__group-box-title { + margin: 0 0 8px; + font-size: 0.72rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--muted, #5c3d37); +} + +.author-profile__stats { + margin: 0; + font-size: 0.85rem; + color: var(--subtle); + font-weight: 600; +} + +@media (max-width: 480px) { + .author-profile { + flex-direction: column; + align-items: center; + text-align: center; + } + + .author-profile__avatar { + width: 72px; + height: 72px; + } +} + +/* ========================================================= + Posts pagination (author archive) + ========================================================= */ + +.posts-pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + font-size: 0.9rem; +} + +.posts-pagination .page-numbers { + color: var(--brand); + font-weight: 600; +} + +.posts-pagination .current { + color: var(--muted); + font-weight: 600; +} + +/* ========================================================= + Print + ========================================================= */ + +@media print { + .site-bar, + .composer-bar, + .composer-panel, + .wall__filters, + .like-btn, + .article-card__footer, + .site-foot { display: none !important; } + .page-shell { width: 100%; padding: 0; } +} + +/* ========================================================= + Utility + ========================================================= */ + +.btn-full { width: 100%; justify-content: center; } + +/* ========================================================= + Login modal + ========================================================= */ + +.login-modal { + border: 0; + border-radius: var(--radius-xl); + padding: 0; + width: min(420px, calc(100% - 24px)); + background: var(--surface); + box-shadow: var(--shadow); + color: var(--text); +} + +.login-modal::backdrop { + background: rgba(38, 24, 16, 0.55); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.login-modal__inner { + padding: 28px 28px 22px; + position: relative; +} + +.login-modal__inner h2 { + margin: 0 0 20px; + font-size: 1.3rem; + text-align: center; + color: var(--text); +} + +.login-modal__close { + position: absolute; + top: 14px; + right: 14px; + width: 32px; + height: 32px; + border: 0; + border-radius: 50%; + background: transparent; + color: var(--muted); + font-size: 1.6rem; + line-height: 1; + cursor: pointer; + transition: background 120ms ease, color 120ms ease; +} + +.login-modal__close:hover { + background: var(--brand-softer); + color: var(--brand); +} + +.login-form { + display: grid; + gap: 18px; +} + +.login-form label { + display: grid; + gap: 8px; + font-weight: 600; + font-size: 0.95rem; + color: var(--text); +} + +.login-form input[type="text"], +.login-form input[type="password"] { + width: 100%; + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + padding: 12px 14px; + background: var(--surface-soft, #f2ebe6); + color: var(--text, #1a0d09); + font: inherit; + transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; +} + +.login-form input:focus { + outline: 0; + border-color: var(--brand, #c0391c); + background: #fff; + box-shadow: 0 0 0 3px rgba(192, 57, 28, 0.14); +} + +.login-modal__remember { + display: flex !important; + flex-direction: row; + align-items: center; + gap: 8px; + font-weight: 400 !important; + font-size: 0.92rem; + cursor: pointer; +} + +.login-modal__forgot { + text-align: center; + margin: 14px 0 0; + font-size: 0.88rem; + color: var(--muted); +} + +/* ========================================================= + Rich text editor toolbar + ========================================================= */ + +.editor-wrap { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + overflow: hidden; + transition: border-color 120ms ease, box-shadow 120ms ease; +} + +.editor-wrap:focus-within { + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(192, 57, 28, 0.15); +} + +.editor-toolbar { + display: flex; + align-items: center; + gap: 2px; + padding: 6px 8px; + border-bottom: 1px solid var(--border); + background: var(--surface-soft); +} + +.editor-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 34px; + height: 34px; + padding: 0 8px; + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: var(--muted); + font-size: 0.9rem; + font-family: inherit; + cursor: pointer; + transition: background 120ms ease, color 120ms ease; +} + +.editor-btn:hover { background: var(--brand-softer); color: var(--brand); } + +.editor-btn--file { font-size: 1rem; } + +.editor-btn--uploading { opacity: 0.45; pointer-events: none; } + +.editor-btn--separator { + width: 1px; + height: 20px; + background: var(--border); + margin: 0 4px; + pointer-events: none; +} + +.compose-editor { + min-height: 120px; + padding: 11px 13px; + outline: none; + color: var(--text); + font: inherit; + font-size: 1rem; + line-height: 1.6; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.compose-editor:empty::before { + content: attr(data-placeholder); + color: var(--subtle); + pointer-events: none; +} + +.compose-editor img { + max-width: 100%; + height: auto; + border-radius: var(--radius-md); + margin: 6px 0; + display: block; +} + +.compose-editor a { color: var(--brand); } + +/* ========================================================= + Link preview card (in modal + in article cards) + ========================================================= */ + +.link-preview-container { margin-top: 10px; } + +.link-preview-card { + display: flex; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + background: var(--surface-soft); + text-decoration: none; + color: inherit; + transition: box-shadow 140ms ease; +} + +.link-preview-card:hover { + box-shadow: var(--shadow-sm); + text-decoration: none; +} + +.link-preview-card__img { + width: 110px; + min-height: 90px; + flex-shrink: 0; + object-fit: cover; + display: block; +} + +.link-preview-card__body { + padding: 11px 13px; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + flex: 1; +} + +.link-preview-card__title { + font-weight: 700; + font-size: 0.93rem; + color: var(--text); + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.link-preview-card__desc { + font-size: 0.82rem; + color: var(--muted); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.link-preview-card__domain { + font-size: 0.76rem; + color: var(--subtle); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-top: auto; +} + +.article-link-preview { + display: block; + margin-top: 12px; + text-decoration: none; +} + +/* ========================================================= + Footer page navigation + ========================================================= */ + +.site-foot__nav { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 4px 16px; + margin-bottom: 10px; +} + +.site-foot__nav a { + font-size: 0.84rem; + color: var(--subtle); + font-weight: 500; + text-decoration: none; + transition: color 120ms ease; +} + +.site-foot__nav a:hover { color: var(--brand); } + +/* ========================================================= + Mobile — bottom-sheet modals, scrollable chips, touch targets + ========================================================= */ + +@media (max-width: 600px) { + /* Bottom-sheet login modal on mobile */ + dialog.login-modal { + inset-block-start: auto !important; + inset-block-end: 0 !important; + inset-inline-start: 0 !important; + inset-inline-end: 0 !important; + width: 100% !important; + max-width: 100% !important; + max-height: 88dvh !important; + border-radius: var(--radius-xl) var(--radius-xl) 0 0 !important; + margin: 0 !important; + } + + /* Scrollable filter row */ + .wall__filters { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + padding-bottom: 4px; + scrollbar-width: none; + } + + .wall__filters::-webkit-scrollbar { display: none; } + + .filter-chip { white-space: nowrap; } + + /* Minimum 44 × 44 px touch targets */ + .filter-chip, + .btn-primary, + .btn-ghost { + min-height: 44px; + } + + .editor-btn { min-height: 44px; min-width: 44px; } + + .like-btn, + .share-btn { min-height: 40px; } + + /* Composer bar — hide event button text on very small screens */ + .composer-bar__event-btn span:not(.composer-bar__event-icon) { + display: none; + } + .composer-bar__event-btn { padding: 10px 12px; } +} + +/* ========================================================= + /ingresar/ — custom login page + ========================================================= */ + +.ingresar-page { + display: flex; + justify-content: center; + padding: 32px 0 60px; +} + +.login-card { + background: var(--surface, #fff); + border: 1px solid var(--border, #c2a89f); + border-top: 4px solid var(--brand, #c0391c); + border-radius: var(--radius-xl, 18px); + box-shadow: var(--shadow-lg, 0 8px 32px rgba(60, 20, 10, 0.13)); + padding: 40px 38px 36px; + width: 100%; + max-width: 420px; +} + +@media (max-width: 480px) { + .login-card { + padding: 32px 24px; + border-radius: var(--radius-lg); + } + + .ingresar-page { + padding: 20px 0 40px; + } +} + +.login-card__title { + margin: 0 0 6px; + font-size: 1.7rem; + text-align: center; + color: var(--text, #1a0d09); + letter-spacing: -0.02em; +} + +.login-card__subtitle { + margin: 0 0 32px; + text-align: center; + color: var(--subtle, #9a7870); + font-size: 0.93rem; +} + +.login-form__remember { + display: flex !important; + flex-direction: row; + align-items: center; + gap: 8px; + font-weight: 400 !important; + font-size: 0.92rem; + cursor: pointer; +} + +.login-card__forgot { + margin: 16px 0 0; + text-align: center; + font-size: 0.88rem; + color: var(--muted); +} + +/* ========================================================= + /tablero/ — dashboard shell + ========================================================= */ + +.tablero__hero { + padding: 18px 0 12px; +} + +.tablero__welcome { + margin: 0; + font-size: 0.9rem; + color: var(--muted); +} + +.tablero__title { + margin: 4px 0 0; + font-size: 1.55rem; +} + +.tablero__tabs { + display: flex; + gap: 2px; + border-bottom: 2px solid var(--border); + margin: 0 0 28px; + padding: 0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} + +.tablero__tabs::-webkit-scrollbar { display: none; } + +.tablero__tab { + padding: 10px 20px; + border: 0; + border-bottom: 3px solid transparent; + margin-bottom: -2px; + background: transparent; + color: var(--muted); + font-weight: 600; + font-size: 0.93rem; + cursor: pointer; + border-radius: 6px 6px 0 0; + transition: color 140ms ease, border-color 140ms ease, background 140ms ease; +} + +.tablero__tab:hover { + color: var(--brand); + background: var(--brand-softer); +} + +.tablero__tab.is-active { + color: var(--brand, #c0391c); + border-bottom-color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); +} + +/* ========================================================= + Tablero — Mensajes tab + ========================================================= */ + +.tablero-mensajes { + display: flex; + flex-direction: column; + gap: 16px; +} + +.notice-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 20px 22px; + box-shadow: var(--shadow-sm); +} + +.notice-card__head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + margin-bottom: 10px; +} + +.notice-card__title { + margin: 0; + font-size: 1.1rem; + line-height: 1.3; +} + +.notice-card__date { + flex-shrink: 0; + font-size: 0.82rem; + color: var(--subtle); + margin-top: 2px; +} + +.notice-card__body { + line-height: 1.65; + color: var(--text); + font-size: 0.97rem; +} + +.notice-card__body p { margin: 0 0 10px; } +.notice-card__body p:last-child { margin-bottom: 0; } + +.tablero-mensajes__empty { + text-align: center; + padding: 48px 0; + color: var(--muted); +} + +/* ========================================================= + Tablero — Publicar tab + ========================================================= */ + +.tablero-publicar__actions { + display: flex; + justify-content: flex-end; + margin-top: 8px; +} + +.tablero-publicar__noperm { + padding: 40px 0; + color: var(--muted); + text-align: center; +} + +/* ========================================================= + Tablero — Perfil tab + ========================================================= */ + +.tablero-perfil { + display: flex; + flex-direction: column; + gap: 28px; +} + +.tablero-perfil__block { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 22px 24px; +} + +.tablero-perfil__section-title { + margin: 0 0 18px; + font-size: 1.05rem; + padding-bottom: 10px; + border-bottom: 1px solid var(--border); +} + +.tablero-perfil__note { + margin: 0; + color: var(--muted); + font-size: 0.9rem; +} + +.tablero-perfil__hint { + font-size: 0.82rem; + font-weight: 400; + color: var(--subtle); +} + +.profile-form { + display: grid; + gap: 18px; +} + +.profile-form label { + display: grid; + gap: 8px; + font-weight: 600; + font-size: 0.95rem; + color: var(--text); +} + +.profile-form input, +.profile-form textarea { + width: 100%; + padding: 12px 14px; + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + background: var(--surface-soft, #f2ebe6); + color: var(--text, #1a0d09); + font: inherit; + font-size: 0.96rem; + transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; +} + +.profile-form input:focus, +.profile-form textarea:focus { + outline: none; + border-color: var(--brand, #c0391c); + background: #fff; + box-shadow: 0 0 0 3px rgba(192, 57, 28, 0.14); +} + +.profile-form textarea { + resize: vertical; + min-height: 90px; +} + +.profile-form__actions { + display: flex; + justify-content: flex-end; + margin-top: 4px; +} + +.group-pills { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.group-pill { + display: inline-flex; + align-items: center; + padding: 5px 14px; + background: var(--brand-soft); + color: var(--brand); + border-radius: 999px; + font-size: 0.88rem; + font-weight: 600; + letter-spacing: 0.01em; +} + +/* ── Profile avatar widget ───────────────────────────────────────────────── */ + +.profile-avatar-wrap { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 4px; +} + +.profile-avatar__img { + width: 96px; + height: 96px; + border-radius: 50%; + object-fit: cover; + border: 3px solid var(--brand-soft, #fbe2db); + flex-shrink: 0; +} + +.profile-avatar__btn { + cursor: pointer; + display: inline-block; + padding: 8px 18px; + font-size: 0.88rem; +} + +.profile-avatar__status { + display: block; + margin-top: 6px; + font-size: 0.82rem; + color: var(--muted, #5c3d37); +} + +/* ── /personas/ page ─────────────────────────────────────────────────────── */ + +.personas-page { + padding: 0 0 80px; +} + +.personas-page__inner { + max-width: 900px; + margin: 0 auto; + padding: 0 20px; +} + +.personas-page__header { + padding: 32px 0 20px; +} + +.personas-page__title { + font-size: 1.8rem; + font-weight: 800; + color: var(--text, #1a0d09); + margin: 0; + letter-spacing: -0.02em; +} + +.personas-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 24px; +} + +.personas-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 18px; +} + +.persona-card { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 10px; + padding: 24px 18px; + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-xl, 18px); + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60, 20, 10, 0.06)); + transition: box-shadow 160ms ease, transform 160ms ease; +} + +.persona-card:hover { + box-shadow: var(--shadow, 0 2px 16px rgba(60, 20, 10, 0.09)); + transform: translateY(-2px); +} + +.persona-card img { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 3px solid var(--brand-soft, #fbe2db); +} + +.persona-card__name { + font-size: 1rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0; + display: block; +} + +.persona-card__bio { + font-size: 0.88rem; + color: var(--muted, #5c3d37); + margin: 0; + line-height: 1.4; +} + +.persona-card__groups { + display: flex; + flex-wrap: wrap; + gap: 6px; + justify-content: center; + margin-top: 4px; +} + +@media (max-width: 600px) { + .personas-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; + } + + .personas-page__title { + font-size: 1.4rem; + } +} + +/* ── Compose form: event fields ──────────────────────────────────────────── */ + +.event-fields[hidden] { display: none !important; } +.cat-picker[hidden] { display: none !important; } + +.event-fields { + display: grid; + gap: 14px; + padding: 14px 16px; + background: var(--brand-softer, #fdf1ed); + border: 1.5px solid var(--brand-soft, #fbe2db); + border-radius: var(--radius-lg, 12px); +} + +.event-fields__datetime { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.event-fields label { + display: grid; + gap: 6px; + font-weight: 600; + font-size: 0.93rem; + color: var(--text, #1a0d09); +} + +.event-fields input[type="date"], +.event-fields input[type="time"], +.event-fields input[type="text"] { + width: 100%; + padding: 10px 13px; + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + background: var(--surface, #fff); + color: var(--text, #1a0d09); + font: inherit; + font-size: 0.95rem; + transition: border-color 140ms ease, box-shadow 140ms ease; +} + +.event-fields input:focus { + outline: none; + border-color: var(--brand, #c0391c); + box-shadow: 0 0 0 3px rgba(192, 57, 28, 0.12); +} + +/* Location autocomplete */ +.event-location-wrap { + position: relative; +} + +.location-suggestions { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: var(--surface, #fff); + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + box-shadow: var(--shadow, 0 2px 16px rgba(60, 20, 10, 0.09)); + list-style: none; + margin: 0; + padding: 4px 0; + z-index: 200; + max-height: 220px; + overflow-y: auto; +} + +.location-suggestions li { + padding: 10px 14px; + font-size: 0.91rem; + color: var(--text, #1a0d09); + cursor: pointer; + line-height: 1.3; +} + +.location-suggestions li:hover, +.location-suggestions li[aria-selected="true"] { + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); +} + +@media (max-width: 480px) { + .event-fields__datetime { + grid-template-columns: 1fr; + } +} + +/* ── Article card: event meta strip ─────────────────────────────────────── */ + +.event-meta-strip { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 10px 14px; + background: var(--brand-softer, #fdf1ed); + border-radius: var(--radius-md, 8px); + margin-bottom: 2px; +} + +.event-meta-strip__item { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.88rem; + font-weight: 600; + color: var(--text, #1a0d09); +} + +.event-meta-strip__icon { + font-size: 1rem; + line-height: 1; +} + +/* ── Calendar dropdown ───────────────────────────────────────────────────── */ + +.cal-dropdown { + position: relative; + display: inline-block; + margin-bottom: 4px; +} + +.cal-dropdown__toggle { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + font-size: 0.85rem; + font-weight: 600; + color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); + border: 1.5px solid var(--brand-soft, #fbe2db); + border-radius: 999px; + cursor: pointer; + transition: background 130ms ease, border-color 130ms ease; +} + +.cal-dropdown__toggle:hover, +.cal-dropdown__toggle[aria-expanded="true"] { + background: var(--brand-soft, #fbe2db); + border-color: var(--brand, #c0391c); +} + +.cal-dropdown__menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 200px; + background: var(--surface, #fff); + border: 1px solid var(--border, #c2a89f); + border-radius: var(--radius-lg, 12px); + box-shadow: var(--shadow, 0 2px 16px rgba(60, 20, 10, 0.09)); + overflow: hidden; + z-index: 100; +} + +.cal-dropdown__item { + display: block; + padding: 11px 16px; + font-size: 0.9rem; + font-weight: 600; + color: var(--text, #1a0d09); + text-decoration: none; + transition: background 120ms ease; +} + +.cal-dropdown__item:hover { + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); +} + +/* ── Compose form: cover image upload ────────────────────────────────────── */ + +.cover-upload { + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 12px; + padding: 18px; + background: var(--surface-soft, #f2ebe6); + border: 2px dashed var(--border, #c2a89f); + border-radius: var(--radius-lg, 12px); + transition: border-color 140ms ease, background 140ms ease; +} + +.cover-upload--dragover { + border-color: var(--brand, #c0391c); + background: var(--brand-softer, #fdf1ed); +} + +.cover-upload--uploading { + opacity: 0.7; + pointer-events: none; +} + +.cover-upload__preview { + width: 100%; + aspect-ratio: 16 / 9; + border-radius: var(--radius-md, 8px); + background-size: cover; + background-position: center; + background-color: var(--surface, #fff); + border: 1px solid var(--border, #c2a89f); +} + +.cover-upload__placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + aspect-ratio: 16 / 9; + border-radius: var(--radius-md, 8px); + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + cursor: pointer; + text-align: center; + padding: 16px; + transition: background 140ms ease; +} + +.cover-upload__placeholder:hover { + background: var(--brand-softer, #fdf1ed); +} + +.cover-upload__placeholder-icon { + font-size: 2.4rem; + line-height: 1; +} + +.cover-upload__placeholder-title { + font-size: 1rem; + font-weight: 700; + color: var(--text, #1a0d09); +} + +.cover-upload__placeholder-hint { + font-size: 0.85rem; + color: var(--muted, #5c3d37); + max-width: 320px; + line-height: 1.4; +} + +.cover-upload__controls { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.cover-upload__btn { + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 18px; +} + +.cover-upload__remove { + padding: 7px 14px; + font-size: 0.85rem; + font-weight: 600; + color: var(--muted, #5c3d37); + background: transparent; + border: 1.5px solid var(--border, #c2a89f); + border-radius: 999px; + cursor: pointer; + transition: background 120ms, color 120ms; +} + +.cover-upload__remove:hover { + background: var(--surface, #fff); + color: var(--brand, #c0391c); + border-color: var(--brand, #c0391c); +} + +/* ── Mis publicaciones (dashboard tab) ───────────────────────────────────── */ + +.mis-pubs__header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 18px; +} + +.mis-pubs__title { + font-size: 1.4rem; + font-weight: 800; + color: var(--text, #1a0d09); + margin: 0; +} + +.mis-pubs__new { + font-size: 0.92rem; + padding: 9px 18px; +} + +.mis-pubs__empty { + text-align: center; + padding: 40px 16px; + color: var(--muted, #5c3d37); + background: var(--surface-soft, #f2ebe6); + border-radius: var(--radius-lg, 12px); +} + +.mis-pubs__empty a { + color: var(--brand, #c0391c); + font-weight: 700; + text-decoration: none; +} + +.mis-pubs__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.mis-pubs__item { + display: grid; + grid-template-columns: 80px 1fr auto; + gap: 16px; + align-items: center; + padding: 14px 16px; + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-lg, 12px); + transition: box-shadow 140ms ease, border-color 140ms ease; +} + +.mis-pubs__item:hover { + border-color: var(--border, #c2a89f); + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60, 20, 10, 0.06)); +} + +.mis-pubs__thumb { + width: 80px; + height: 60px; + border-radius: var(--radius-md, 8px); + background-size: cover; + background-position: center; + background-color: var(--surface-soft, #f2ebe6); + display: block; +} + +.mis-pubs__thumb--empty { + display: flex; + align-items: center; + justify-content: center; + font-size: 1.6rem; + color: var(--subtle, #9a7870); +} + +.mis-pubs__body { + min-width: 0; +} + +.mis-pubs__meta { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + margin-bottom: 4px; +} + +.mis-pubs__kind { + padding: 2px 9px; + background: var(--brand-soft, #fbe2db); + color: var(--brand, #c0391c); + font-size: 0.66rem; + font-weight: 800; + letter-spacing: 0.05em; + border-radius: 999px; + text-transform: uppercase; +} + +.mis-pubs__status { + padding: 2px 8px; + background: var(--surface-soft, #f2ebe6); + color: var(--muted, #5c3d37); + font-size: 0.7rem; + font-weight: 700; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.mis-pubs__heading { + font-size: 1rem; + font-weight: 700; + margin: 0 0 4px; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.mis-pubs__heading a { + color: var(--text, #1a0d09); + text-decoration: none; +} + +.mis-pubs__heading a:hover { + color: var(--brand, #c0391c); +} + +.mis-pubs__dates { + font-size: 0.78rem; + color: var(--muted, #5c3d37); + margin: 0; + display: flex; + flex-wrap: wrap; + gap: 4px 8px; +} + +.mis-pubs__dates-edit { + color: var(--subtle, #9a7870); + font-style: italic; +} + +.mis-pubs__actions { + display: flex; + gap: 8px; + align-items: center; +} + +.mis-pubs__edit { + font-size: 0.85rem; + padding: 7px 14px; +} + +.mis-pubs__delete { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + font-size: 0.85rem; + font-weight: 600; + padding: 7px 14px; + color: var(--muted, #5c3d37); + background: transparent; + border: 1.5px solid var(--border, #c2a89f); + border-radius: 999px; + cursor: pointer; + transition: background 120ms, color 120ms, border-color 120ms; +} + +.mis-pubs__delete:hover:not(:disabled) { + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); + border-color: var(--brand, #c0391c); +} + +.mis-pubs__delete:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* "Edited" badge on single post */ +.article-card__edited { + color: var(--subtle, #9a7870); + font-size: 0.85em; + font-style: italic; + margin-left: 4px; +} + +@media (max-width: 600px) { + .mis-pubs__item { + grid-template-columns: 64px 1fr; + grid-template-rows: auto auto; + } + .mis-pubs__thumb { + width: 64px; + height: 48px; + } + .mis-pubs__actions { + grid-column: 1 / -1; + justify-content: flex-end; + } +} + +/* ── Article card variant: EVENT ─────────────────────────────────────────── */ + +.article-card--event { + padding: 18px 22px; + border-left: 5px solid var(--brand, #c0391c); + background: linear-gradient(135deg, var(--brand-softer, #fdf1ed) 0%, var(--surface, #fff) 55%); +} + +.article-card--event .article-card__cover-link { + margin: 0 -24px 14px; +} + +.article-card--event .article-card__cover { + height: 160px; +} + +.article-card--event .article-card__head { + margin-bottom: 10px; +} + +.article-card--event .article-card__title { + font-size: 1.15rem; + margin: 0 0 8px; +} + +.article-card--event .article-card__excerpt { + font-size: 0.92rem; + margin: 0 0 12px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.article-card--event .event-meta-strip { + margin: 0 0 12px; + padding: 8px 12px; + background: rgba(192, 57, 28, 0.08); +} + +.article-card--event .event-meta-strip__item { + font-size: 0.85rem; +} + +.article-card--event .article-card__footer { + margin-top: 14px; + padding-top: 12px; +} + +/* Calendar SVG icon — locked to pure white for maximum contrast on red button */ +.cal-icon { + flex-shrink: 0; + vertical-align: middle; + display: inline-block; +} + +.cal-icon path, +.cal-icon rect, +.cal-icon circle { + stroke: #ffffff; +} + +.cal-icon circle[fill], +.cal-icon rect[fill], +.cal-icon path[fill] { + fill: #ffffff; +} + +/* Add-to-Calendar trigger button — prominent CTA inside event cards */ +/* "Add to calendar" — compact icon-only circle in the card footer */ +.atcb-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + flex-shrink: 0; + background: var(--brand, #c0391c); + border: none; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 2px 8px rgba(192, 57, 28, 0.22); + transition: background 140ms ease, transform 100ms ease; +} + +.atcb-trigger:hover, +.atcb-trigger:focus-visible { + background: var(--brand-dark, #9a2a0f); + transform: translateY(-1px); + outline: none; +} + +.atcb-trigger:active { transform: translateY(0); } + +.cal-dropdown--prominent .cal-dropdown__toggle { + width: 100%; + justify-content: center; + padding: 13px 22px; + font-size: 0.98rem; + font-weight: 700; + letter-spacing: 0.01em; + color: #fff; + background: var(--brand, #c0391c); + border-color: var(--brand, #c0391c); + border-radius: var(--radius-lg, 12px); + box-shadow: 0 2px 10px rgba(192, 57, 28, 0.25); +} + +.cal-dropdown--prominent .cal-dropdown__toggle:hover, +.cal-dropdown--prominent .cal-dropdown__toggle[aria-expanded="true"] { + background: var(--brand-dark, #9a2a0f); + border-color: var(--brand-dark, #9a2a0f); + box-shadow: 0 4px 16px rgba(192, 57, 28, 0.35); + transform: translateY(-1px); +} + +.cal-dropdown--prominent .cal-dropdown__menu { + left: 50%; + transform: translateX(-50%); + min-width: 240px; +} + +/* ========================================================= + @Mention autocomplete dropdown + ========================================================= */ + +.mention-list { + position: absolute; + z-index: 9000; + min-width: 200px; + max-width: 280px; + background: var(--surface, #fff); + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + box-shadow: var(--shadow, 0 2px 16px rgba(60,20,10,0.09)); + list-style: none; + margin: 0; + padding: 4px 0; + overflow: hidden; +} + +.mention-list li { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + cursor: pointer; + font-size: 0.9rem; + color: var(--text, #1a0d09); + transition: background 120ms ease; +} + +.mention-list li:hover, +.mention-list li.is-active { + background: var(--surface-soft, #f2ebe6); +} + +.mention-list__avatar { + width: 28px; + height: 28px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.mention-list__name { + font-weight: 600; +} + +/* Inline mention span in the editor */ +.mention { + display: inline-block; + background: var(--brand-softer, #fdf1ed); + color: var(--brand, #c0391c); + border-radius: var(--radius-sm, 5px); + padding: 1px 5px; + font-weight: 700; + font-size: 0.92em; + user-select: none; + cursor: default; +} + +/* ========================================================= + Notification tab badge + ========================================================= */ + +.tab-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 4px; + background: var(--brand, #c0391c); + color: #fff; + font-size: 0.7rem; + font-weight: 800; + border-radius: 999px; + margin-left: 5px; + vertical-align: middle; + line-height: 1; +} + +/* ========================================================= + Notification list (tablero-notificaciones) + ========================================================= */ + +.notif-panel { + padding: 6px 0; +} + +.notif-panel__head { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 18px; +} + +.notif-panel__title { + font-size: 1.2rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0; +} + +.notif-panel__push-btn { + font-size: 0.82rem; + padding: 7px 14px; +} + +.notif-panel__push-status { + font-size: 0.88rem; + color: var(--muted, #5c3d37); + margin: 0 0 12px; +} + +.notif-panel__empty { + color: var(--subtle, #9a7870); + text-align: center; + padding: 40px 0; +} + +.notif-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.notif-card { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 18px; + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-lg, 12px); + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60,20,10,0.06)); +} + +.notif-card--unread { + background: var(--brand-softer, #fdf1ed); + border-color: var(--brand-soft, #fbe2db); +} + +.notif-card__dot { + flex-shrink: 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: transparent; + margin-top: 6px; +} + +.notif-card--unread .notif-card__dot { + background: var(--brand, #c0391c); +} + +.notif-card__body { + flex: 1; + min-width: 0; +} + +.notif-card__message, +a.notif-card__message { + display: block; + font-size: 0.93rem; + color: var(--text, #1a0d09); + font-weight: 600; + text-decoration: none; +} + +a.notif-card__message:hover { + color: var(--brand, #c0391c); +} + +.notif-card__time { + display: block; + font-size: 0.78rem; + color: var(--subtle, #9a7870); + margin-top: 4px; +} + +/* ========================================================= + Agenda calendar (tablero-agenda) + ========================================================= */ + +.cal-month { + position: relative; +} + +.cal-month__nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.cal-month__nav-btn { + padding: 8px 12px; + font-size: 0.88rem; +} + +.cal-month__heading { + font-size: 1.1rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0; + min-width: 160px; + text-align: center; +} + +.cal-month__weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + margin-bottom: 4px; +} + +.cal-month__weekdays span { + text-align: center; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--subtle, #9a7870); + padding: 4px 0; +} + +.cal-month__grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +.cal-day { + position: relative; + min-height: 44px; + padding: 6px 4px; + border-radius: var(--radius-sm, 5px); + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + background: var(--surface, #fff); + border: 1px solid transparent; + transition: background 130ms ease, border-color 130ms ease; +} + +.cal-day--empty { + background: transparent; +} + +.cal-day--today .cal-day__num { + background: var(--brand, #c0391c); + color: #fff; + border-radius: 50%; + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; +} + +.cal-day--has-event { + background: var(--brand-softer, #fdf1ed); + border-color: var(--brand-soft, #fbe2db); +} + +.cal-day--has-event:hover { + background: var(--brand-soft, #fbe2db); + border-color: var(--brand, #c0391c); +} + +.cal-day__num { + font-size: 0.85rem; + font-weight: 600; + color: var(--text, #1a0d09); + line-height: 1; +} + +.cal-day--empty .cal-day__num { + color: transparent; +} + +.cal-day__dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--brand, #c0391c); + flex-shrink: 0; +} + +/* Event list — shown directly under the calendar grid */ +.cal-event-list { + margin-top: 22px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.cal-event-list__empty, +.cal-event-list__loading { + color: var(--subtle, #9a7870); + text-align: center; + font-size: 0.9rem; + padding: 24px 0; + margin: 0; +} + +.cal-event-item { + position: relative; + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-left: 4px solid var(--cal-event-color, var(--brand, #c0391c)); + border-radius: var(--radius-lg, 12px); + padding: 18px 20px 16px; + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60,20,10,0.06)); + transition: box-shadow 160ms ease, transform 160ms ease, background 200ms ease; +} + +.cal-event-item--flash { + background: var(--brand-softer, #fdf1ed); + box-shadow: 0 0 0 3px var(--brand-soft, #fbe2db), var(--shadow, 0 2px 16px rgba(60,20,10,0.09)); +} + +.cal-event-item__date { + font-size: 1.05rem; + font-weight: 800; + color: var(--cal-event-color, var(--brand, #c0391c)); + letter-spacing: -0.01em; + margin-bottom: 6px; +} + +.cal-event-item__title { + font-size: 1.15rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0 0 6px; + line-height: 1.3; +} + +.cal-event-item__location { + font-size: 0.85rem; + color: var(--muted, #5c3d37); + margin: 0 0 8px; +} + +.cal-event-item__desc { + font-size: 0.92rem; + color: var(--muted, #5c3d37); + line-height: 1.5; + margin: 6px 0 14px; +} + +.cal-event-item__footer { + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 12px; +} + +.cal-event-item__footer .atcb-trigger { + background: var(--cal-event-color, var(--brand, #c0391c)); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); +} + +.cal-event-item__footer .atcb-trigger:hover, +.cal-event-item__footer .atcb-trigger:focus-visible { + filter: brightness(0.92); + background: var(--cal-event-color, var(--brand, #c0391c)); +} + +/* ========================================================= + Comment count button (post-card footer) + ========================================================= */ + +.comment-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 6px 10px; + border-radius: 999px; + background: transparent; + border: 1.5px solid var(--border, #c2a89f); + color: var(--muted, #5c3d37); + font-size: 0.82rem; + font-weight: 600; + text-decoration: none; + transition: background 130ms ease, color 130ms ease, border-color 130ms ease; + cursor: pointer; +} + +.comment-btn:hover { + background: var(--surface-soft, #f2ebe6); + border-color: var(--muted, #5c3d37); + color: var(--text, #1a0d09); +} + +.comment-icon { flex-shrink: 0; } +.comment-count { min-width: 1ch; } + +/* ========================================================= + Comments section (single post) + ========================================================= */ + +.comments-section { + margin-top: 40px; + padding-top: 28px; + border-top: 1.5px solid var(--border-light, #e0cec8); +} + +.comments-section__title { + font-size: 1.25rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0 0 20px; +} + +.comments-section__empty { + color: var(--subtle, #9a7870); + margin: 0 0 28px; +} + +.comment-list { + list-style: none; + margin: 0 0 32px; + padding: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.comment-list .children { + list-style: none; + margin: 10px 0 0 32px; + padding: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.comment-card { + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-lg, 12px); + padding: 16px 18px; + box-shadow: var(--shadow-sm, 0 1px 6px rgba(60,20,10,0.06)); +} + +.comment-card__body { display: contents; } + +.comment-card__head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 10px; +} + +.comment-card__avatar { + width: 40px !important; + height: 40px !important; + border-radius: 50% !important; + object-fit: cover; + flex-shrink: 0; +} + +.comment-card__meta { + display: flex; + flex-direction: column; + gap: 2px; +} + +.comment-card__author { + font-weight: 700; + font-size: 0.9rem; + color: var(--text, #1a0d09); +} + +.comment-card__time { + font-size: 0.77rem; + color: var(--subtle, #9a7870); +} + +.comment-card__content { + font-size: 0.93rem; + line-height: 1.55; + color: var(--text, #1a0d09); +} + +.comment-card__content p { margin: 0 0 8px; } +.comment-card__content p:last-child { margin-bottom: 0; } + +.comment-card__footer { + margin-top: 10px; +} + +.comment-reply-link { + font-size: 0.8rem; + font-weight: 600; + color: var(--muted, #5c3d37); + text-decoration: none; + transition: color 120ms ease; +} + +.comment-reply-link:hover { color: var(--brand, #c0391c); } + +/* WP comment form */ +#respond { + margin-top: 28px; + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-xl, 18px); + padding: 24px 26px 20px; + box-shadow: var(--shadow-sm); +} + +#reply-title { + font-size: 1.1rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0 0 18px; +} + +#reply-title small { font-size: 0.78rem; font-weight: 400; margin-left: 8px; } +#reply-title small a { color: var(--subtle); } + +.comment-form p { margin: 0 0 14px; } + +.comment-form label { + display: block; + font-size: 0.85rem; + font-weight: 600; + color: var(--muted, #5c3d37); + margin-bottom: 5px; +} + +.comment-form input[type="text"], +.comment-form input[type="email"], +.comment-form input[type="url"], +.comment-form textarea { + width: 100%; + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + padding: 10px 13px; + font-family: inherit; + font-size: 0.93rem; + background: var(--surface-soft, #f2ebe6); + color: var(--text, #1a0d09); + transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease; +} + +.comment-form input:focus, +.comment-form textarea:focus { + outline: none; + border-color: var(--brand, #c0391c); + background: #fff; + box-shadow: 0 0 0 3px rgba(192,57,28,0.12); +} + +.comment-form-comment textarea { min-height: 100px; resize: vertical; } + +.form-submit { margin: 0; } +.form-submit .btn-primary { width: auto; } + +/* ========================================================= + Contact form (tablero-mensajes) + ========================================================= */ + +.contact-form-section { + margin-top: 36px; + padding-top: 28px; + border-top: 1.5px solid var(--border-light, #e0cec8); +} + +.contact-form-section__title { + font-size: 1.2rem; + font-weight: 700; + color: var(--text, #1a0d09); + margin: 0 0 6px; +} + +.contact-form-section__intro { + font-size: 0.9rem; + color: var(--muted, #5c3d37); + margin: 0 0 22px; +} + +.contact-form { + background: var(--surface, #fff); + border: 1px solid var(--border-light, #e0cec8); + border-radius: var(--radius-xl, 18px); + padding: 24px 26px 20px; + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + gap: 16px; +} + +.contact-form .form-row { + display: flex; + flex-direction: column; + gap: 5px; +} + +.contact-form label { + font-size: 0.85rem; + font-weight: 600; + color: var(--muted, #5c3d37); +} + +.contact-form select, +.contact-form input[type="text"], +.contact-form textarea { + border: 1.5px solid var(--border, #c2a89f); + border-radius: var(--radius-md, 8px); + padding: 10px 13px; + font-family: inherit; + font-size: 0.93rem; + background: var(--surface-soft, #f2ebe6); + color: var(--text, #1a0d09); + transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease; + width: 100%; +} + +.contact-form select:focus, +.contact-form input:focus, +.contact-form textarea:focus { + outline: none; + border-color: var(--brand, #c0391c); + background: #fff; + box-shadow: 0 0 0 3px rgba(192,57,28,0.12); +} + +.contact-form textarea { min-height: 110px; resize: vertical; } + +.contact-form .form-actions { + display: flex; + justify-content: flex-end; + padding-top: 4px; +} + +/* ========================================================= + Mobile-first improvements + ========================================================= */ + +/* Feed: single column with tighter padding on small screens */ +@media (max-width: 480px) { + .article-card { + padding: 16px 16px; + border-radius: var(--radius-lg, 12px); + } + + .article-card__cover-link { + margin: 0 -16px 12px; + border-radius: var(--radius-lg, 12px) var(--radius-lg, 12px) 0 0; + } + + .article-card--event .article-card__cover-link { + margin: 0 -16px 12px; + } + + .article-card__title { font-size: 1.1rem; } + + .article-card__footer { + flex-wrap: wrap; + gap: 6px; + } + + /* Stack the event meta strip items */ + .event-meta-strip { + flex-direction: column; + gap: 6px; + } +} + +/* Dashboard tabs: reduce padding and allow scrolling without clipping */ +@media (max-width: 600px) { + .tablero__tab { + padding: 8px 12px; + font-size: 0.82rem; + white-space: nowrap; + } + + .tablero__tabs { + margin-bottom: 20px; + } + + .tablero__hero { padding: 12px 0 8px; } + .tablero__title { font-size: 1.25rem; } +} + +/* Composer panel: full-width on mobile */ +@media (max-width: 600px) { + .composer-panel__inner { + border-radius: var(--radius-lg, 12px); + } + + .composer-panel.is-open .composer-panel__inner { + padding: 18px 16px 16px; + } + + .composer-panel__actions { + flex-direction: column; + gap: 8px; + } + + .composer-panel__actions .btn-ghost, + .composer-panel__actions .btn-primary { + width: 100%; + text-align: center; + } +} + +/* Contact and comments: full bleed on very small screens */ +@media (max-width: 480px) { + .contact-form, + #respond { + padding: 18px 16px 16px; + border-radius: var(--radius-lg, 12px); + } + + .comment-card { + padding: 14px 14px; + } + + .comment-list .children { + margin-left: 18px; + } +} + +/* Filter chips row: wrap but stay readable */ +@media (max-width: 480px) { + #filters { + gap: 6px; + } + + .filter-chip { + padding: 6px 12px; + font-size: 0.8rem; + } +} + +/* Notification panel: tighter heading row on small screens */ +@media (max-width: 480px) { + .notif-panel__head { + flex-direction: column; + align-items: flex-start; + } + + .notif-card { + padding: 12px 14px; + } +} + +/* Calendar: smaller cells on mobile */ +@media (max-width: 480px) { + .cal-month__nav-btn { padding: 6px 8px; } + .cal-day { min-height: 36px; padding: 4px 2px; } + .cal-day__num { font-size: 0.75rem; } + .cal-event-item { padding: 14px 16px 14px; } + .cal-event-item__date { font-size: 0.95rem; } + .cal-event-item__title { font-size: 1.02rem; } +} + +/* Site bar: prevent overflow on very narrow screens */ +@media (max-width: 360px) { + .site-bar__inner { padding: 8px 12px; } + .site-bar__btn { padding: 7px 12px; font-size: 0.82rem; } +} diff --git a/themes/vienalatina/layouts/_default/baseof.html b/themes/vienalatina/layouts/_default/baseof.html new file mode 100644 index 0000000..22523da --- /dev/null +++ b/themes/vienalatina/layouts/_default/baseof.html @@ -0,0 +1,25 @@ + + + + + + {{ if not .IsHome }}{{ .Title }} — {{ end }}{{ .Site.Title }} + {{ partial "seo-head.html" . }} + {{ $css := resources.Get "css/main.css" | minify | fingerprint }} + + + {{ range .AlternativeOutputFormats -}} + + {{ end -}} + + + +{{ partial "header.html" . }} + +
+ {{ block "main" . }}{{ end }} + {{ partial "footer.html" . }} +
+ + + diff --git a/themes/vienalatina/layouts/_default/list.html b/themes/vienalatina/layouts/_default/list.html new file mode 100644 index 0000000..242de36 --- /dev/null +++ b/themes/vienalatina/layouts/_default/list.html @@ -0,0 +1,19 @@ +{{/* Category / archive listing — article-card markup from the WP theme feed. */}} +{{ define "main" }} +
+

{{ .Title }}

+ + {{ range .Pages }} +
+ {{ end }} +
+{{ end }} diff --git a/themes/vienalatina/layouts/_default/single.html b/themes/vienalatina/layouts/_default/single.html new file mode 100644 index 0000000..5df7a21 --- /dev/null +++ b/themes/vienalatina/layouts/_default/single.html @@ -0,0 +1,40 @@ +{{/* Article template — ported from theme/single.php (minus likes/comments/login, + which have no place on a static site). */}} +{{ define "main" }} +
+
+ +
+
+
+ {{ range first 1 (.GetTerms "categories") }} + {{ .LinkTitle }} + {{ end }} + +
+
+
+ +

{{ .Title }}

+ + {{ with .Description }} +

{{ . }}

+ {{ end }} + + {{ with .Params.image }} + + {{ end }} + +
+ {{ .Content }} +
+ + + ← Volver al inicio + + +
+
+{{ end }} diff --git a/themes/vienalatina/layouts/index.html b/themes/vienalatina/layouts/index.html new file mode 100644 index 0000000..51c2ee8 --- /dev/null +++ b/themes/vienalatina/layouts/index.html @@ -0,0 +1,17 @@ +{{/* Home: feed of recent articles in the current language. */}} +{{ define "main" }} +
+ {{ range first 20 (where .Site.RegularPages "Section" "post") }} +
+
+ {{ range first 1 (.GetTerms "categories") }} + {{ .LinkTitle }} + {{ end }} + +
+

{{ .Title }}

+

{{ .Description | default (.Summary | plainify | truncate 200) }}

+
+ {{ end }} +
+{{ end }} diff --git a/themes/vienalatina/layouts/index.llms.txt b/themes/vienalatina/layouts/index.llms.txt new file mode 100644 index 0000000..6caeec3 --- /dev/null +++ b/themes/vienalatina/layouts/index.llms.txt @@ -0,0 +1,13 @@ +# {{ .Site.Title }} + +> {{ .Site.Params.description }} + +Site home: {{ .Site.Home.Permalink }} + +This site publishes in Spanish. Every article has server-rendered German (/de/) and Brazilian Portuguese (/pt-br/) translations, linked via hreflang. + +## Recent articles + +{{ range first 50 (where .Site.RegularPages "Section" "post") -}} +- [{{ .Title }}]({{ .Permalink }}): {{ .Description | default (.Summary | plainify | truncate 200) }} +{{ end -}} diff --git a/themes/vienalatina/layouts/partials/footer.html b/themes/vienalatina/layouts/partials/footer.html new file mode 100644 index 0000000..e736803 --- /dev/null +++ b/themes/vienalatina/layouts/partials/footer.html @@ -0,0 +1,12 @@ +{{/* Ported from theme/footer.php */}} +
+ {{ $pages := where .Site.RegularPages "Section" "page" }} + {{ if $pages }} + + {{ end }} +

{{ .Site.Title }} © {{ now.Format "2006" }}

+
diff --git a/themes/vienalatina/layouts/partials/header.html b/themes/vienalatina/layouts/partials/header.html new file mode 100644 index 0000000..d46496f --- /dev/null +++ b/themes/vienalatina/layouts/partials/header.html @@ -0,0 +1,25 @@ +{{/* Ported from theme/header.php — static version: no login/account controls. + No Google Fonts: the font-family stack in main.css falls back to system + sans-serif, keeping the site free of third-party requests (GDPR). */}} +
+
+ + {{ .Site.Title }} + + + +
+ + +
diff --git a/themes/vienalatina/layouts/partials/lang-switcher.html b/themes/vienalatina/layouts/partials/lang-switcher.html new file mode 100644 index 0000000..648674c --- /dev/null +++ b/themes/vienalatina/layouts/partials/lang-switcher.html @@ -0,0 +1,17 @@ +{{/* Ported from plataforma_render_lang_switcher() in theme/functions.php. + Three real links to sibling URLs — crawlable, no JS. */}} +{{ if .IsTranslated }} +{{ $labels := dict "es" "ES" "de" "DE" "pt-br" "PT" }} + +{{ end }} diff --git a/themes/vienalatina/layouts/partials/seo-head.html b/themes/vienalatina/layouts/partials/seo-head.html new file mode 100644 index 0000000..1ab31a8 --- /dev/null +++ b/themes/vienalatina/layouts/partials/seo-head.html @@ -0,0 +1,85 @@ +{{/* GEO/SEO surfaces — ported from theme/inc/seo.php: + hreflang alternates, Open Graph + og:locale(:alternate), Twitter Card, + JSON-LD BlogPosting (articles) / Blog (home), all with inLanguage. */}} +{{ $localeMap := .Site.Params.localeMap }} +{{ $locale := index $localeMap .Language.Lang | default "es_ES" }} +{{ $desc := "" }} +{{ if .IsPage }}{{ $desc = .Description | default (.Summary | plainify | truncate 200) }}{{ else }}{{ $desc = .Site.Params.description }}{{ end }} + + + +{{/* hreflang — one alternate per language sibling + x-default to Spanish */}} +{{ if .IsTranslated }} +{{ range .AllTranslations }} + +{{ end }} +{{ range .AllTranslations }}{{ if eq .Language.Lang "es" }} + +{{ end }}{{ end }} +{{ end }} + +{{ if and .IsPage (eq .Section "post") }} +{{/* ---- Article ---- */}} + + + + + + +{{ range .Translations }} +{{ with index $localeMap .Language.Lang }}{{ end }} +{{ end }} +{{ with .Params.image }}{{ end }} + + + + + +{{ $schema := dict + "@context" "https://schema.org" + "@type" "BlogPosting" + "inLanguage" .Site.Language.LanguageCode + "headline" .Title + "description" $desc + "url" .Permalink + "datePublished" (.Date.Format "2006-01-02T15:04:05-07:00") + "dateModified" (.Lastmod.Format "2006-01-02T15:04:05-07:00") + "author" (dict "@type" "Person" "name" (.Params.author | default .Site.Title)) + "publisher" (dict "@type" "Organization" "name" .Site.Title "url" .Site.Home.Permalink) +}} +{{ with .Params.image }}{{ $schema = merge $schema (dict "image" (. | absURL)) }}{{ end }} + + +{{ else if .IsHome }} +{{/* ---- Home ---- */}} + + + + + + + +{{ $posts := first 10 (where .Site.RegularPages "Section" "post") }} +{{ $blogPost := slice }} +{{ range $posts }} +{{ $entry := dict + "@type" "BlogPosting" + "headline" .Title + "url" .Permalink + "datePublished" (.Date.Format "2006-01-02T15:04:05-07:00") + "author" (dict "@type" "Person" "name" (.Params.author | default .Site.Title)) +}} +{{ with .Description }}{{ $entry = merge $entry (dict "description" .) }}{{ end }} +{{ $blogPost = $blogPost | append $entry }} +{{ end }} +{{ $schema := dict + "@context" "https://schema.org" + "@type" "Blog" + "name" .Site.Title + "description" $desc + "url" .Permalink + "inLanguage" .Site.Language.LanguageCode +}} +{{ if $blogPost }}{{ $schema = merge $schema (dict "blogPost" $blogPost) }}{{ end }} + +{{ end }} diff --git a/themes/vienalatina/theme.toml b/themes/vienalatina/theme.toml new file mode 100644 index 0000000..80f6ce4 --- /dev/null +++ b/themes/vienalatina/theme.toml @@ -0,0 +1,4 @@ +name = "Viena Latina" +license = "MIT" +description = "Static port of the Plataforma WordPress theme for vienalatina.com — same CSS, three languages, zero third-party requests." +min_version = "0.120.0"