feat: Hugo engine for iGaming affiliate template (layouts, assets, i18n, verified data, CI build script)
@@ -0,0 +1,21 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# .env.example — copy to .env and fill in per-deployment secrets.
|
||||||
|
# Hugo does NOT read .env automatically; these are consumed by CI/host build
|
||||||
|
# settings or exported into the shell before building. Keep .env out of git.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Public base URL for the deployed site (overrides hugo.toml baseURL in CI).
|
||||||
|
# Example: https://www.brandname.com/
|
||||||
|
HUGO_BASEURL="https://example.com/"
|
||||||
|
|
||||||
|
# Hugo build environment ("production" enables production optimisations).
|
||||||
|
HUGO_ENV="production"
|
||||||
|
|
||||||
|
# Optional analytics IDs. These are loaded by the cookie-consent gated loader
|
||||||
|
# only AFTER the visitor accepts the relevant category.
|
||||||
|
ANALYTICS_GA4_ID=""
|
||||||
|
ANALYTICS_PLAUSIBLE_DOMAIN=""
|
||||||
|
|
||||||
|
# Optional: default affiliate sub-id override for campaign-level tracking.
|
||||||
|
# Per-page sub-ids are still generated automatically by cta-button.html.
|
||||||
|
AFFILIATE_CAMPAIGN_ID=""
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Hugo build output & caches
|
||||||
|
/public/
|
||||||
|
/resources/
|
||||||
|
.hugo_build.lock
|
||||||
|
|
||||||
|
# Standalone CSS build artifact (npm run build:css)
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# Env & secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# OS / editor
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/* =========================================================================
|
||||||
|
main.css — Hugo Pipes entry (processed with resources.ExecuteAsTemplate |
|
||||||
|
css.PostCSS | fingerprint). The :root block below is injected from
|
||||||
|
data/brand.yaml so every brand colour flows through Tailwind utilities
|
||||||
|
(bg-primary, text-primary, ...) with zero template edits.
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* --- Brand tokens injected from data/brand.yaml --------------------------
|
||||||
|
Both a hex value (for gradients/SVG) and an "R G B" channel triplet (for
|
||||||
|
Tailwind alpha modifiers) are emitted for each brand colour. Colours in
|
||||||
|
data/brand.yaml MUST be full 6-digit hex (e.g. #0057FF). */
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
{{- $hexmap := dict "0" 0 "1" 1 "2" 2 "3" 3 "4" 4 "5" 5 "6" 6 "7" 7 "8" 8 "9" 9 "a" 10 "b" 11 "c" 12 "d" 13 "e" 14 "f" 15 -}}
|
||||||
|
{{- $colors := dict "primary" $brand.primaryColor "secondary" $brand.secondaryColor "primary-contrast" ($brand.primaryContrast | default "#FFFFFF") -}}
|
||||||
|
:root {
|
||||||
|
{{- range $name, $hex := $colors }}
|
||||||
|
{{- /* Normalise: strip '#', lowercase, expand 3-digit shorthand (#05f -> #0055ff). */ -}}
|
||||||
|
{{- $h := lower (strings.TrimPrefix "#" $hex) }}
|
||||||
|
{{- if eq (len $h) 3 }}
|
||||||
|
{{- $h = printf "%s%s%s%s%s%s" (substr $h 0 1) (substr $h 0 1) (substr $h 1 1) (substr $h 1 1) (substr $h 2 1) (substr $h 2 1) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- /* Validate: exactly 6 hex characters, else fail the build naming the colour. */ -}}
|
||||||
|
{{- $ok := eq (len $h) 6 }}
|
||||||
|
{{- range $ch := (split $h "") }}{{- if not (isset $hexmap $ch) }}{{- $ok = false }}{{- end }}{{- end }}
|
||||||
|
{{- if not $ok }}{{- errorf "brand.yaml: %s colour %q is not a valid 3- or 6-digit hex value (e.g. #0057FF)" $name $hex }}{{- end }}
|
||||||
|
--brand-{{ $name }}: #{{ $h }};
|
||||||
|
{{- if $ok }}
|
||||||
|
--brand-{{ $name }}-rgb: {{ add (mul (index $hexmap (substr $h 0 1)) 16) (index $hexmap (substr $h 1 1)) }} {{ add (mul (index $hexmap (substr $h 2 1)) 16) (index $hexmap (substr $h 3 1)) }} {{ add (mul (index $hexmap (substr $h 4 1)) 16) (index $hexmap (substr $h 5 1)) }};
|
||||||
|
{{- else }}
|
||||||
|
--brand-{{ $name }}-rgb: 0 0 0;
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Base layer --------------------------------------------------------- */
|
||||||
|
@layer base {
|
||||||
|
/* Alpine.js: hide elements until initialised to avoid flashes. */
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-white text-secondary antialiased;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5 {
|
||||||
|
@apply font-bold tracking-tight text-secondary;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
@apply text-primary underline-offset-2;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
/* Visible keyboard focus for accessibility */
|
||||||
|
:focus-visible {
|
||||||
|
@apply outline outline-2 outline-offset-2 outline-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Components --------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.site-container {
|
||||||
|
@apply mx-auto w-full max-w-6xl px-4 sm:px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons ------------------------------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center gap-2 rounded-lg px-5 py-3 text-base font-semibold no-underline transition-transform duration-150 focus-visible:outline-2;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
@apply -translate-y-0.5 no-underline;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn bg-primary text-primary-contrast shadow-sm hover:shadow-md;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
@apply btn border-2 border-primary bg-transparent text-primary hover:bg-primary hover:text-primary-contrast;
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
@apply btn bg-secondary/5 text-secondary hover:bg-secondary/10;
|
||||||
|
}
|
||||||
|
.btn-lg {
|
||||||
|
@apply px-7 py-4 text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards --------------------------------------------------------------- */
|
||||||
|
.card {
|
||||||
|
@apply rounded-xl border border-secondary/10 bg-white p-6 shadow-sm;
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
@apply transition-shadow duration-150 hover:shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges -------------------------------------------------------------- */
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold;
|
||||||
|
}
|
||||||
|
.badge-primary {
|
||||||
|
@apply badge bg-primary/10 text-primary;
|
||||||
|
}
|
||||||
|
.badge-muted {
|
||||||
|
@apply badge bg-secondary/10 text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section helpers ----------------------------------------------------- */
|
||||||
|
.section {
|
||||||
|
@apply py-12 sm:py-16;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
@apply text-sm font-semibold uppercase tracking-wider text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rendered markdown body --------------------------------------------- */
|
||||||
|
.prose-content {
|
||||||
|
@apply max-w-none text-[1.05rem] leading-relaxed text-secondary/90;
|
||||||
|
}
|
||||||
|
.prose-content h2 {
|
||||||
|
@apply mb-4 mt-10 text-2xl font-bold text-secondary;
|
||||||
|
}
|
||||||
|
.prose-content h3 {
|
||||||
|
@apply mb-3 mt-8 text-xl font-semibold text-secondary;
|
||||||
|
}
|
||||||
|
.prose-content p {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
.prose-content ul {
|
||||||
|
@apply mb-4 list-disc space-y-1 pl-6;
|
||||||
|
}
|
||||||
|
.prose-content ol {
|
||||||
|
@apply mb-4 list-decimal space-y-1 pl-6;
|
||||||
|
}
|
||||||
|
/* Exclude affiliate CTA buttons so their button styling is preserved. */
|
||||||
|
.prose-content a:not([data-affiliate]) {
|
||||||
|
@apply font-medium text-primary underline;
|
||||||
|
}
|
||||||
|
.prose-content a[data-affiliate] {
|
||||||
|
@apply my-2 no-underline;
|
||||||
|
}
|
||||||
|
.prose-content blockquote {
|
||||||
|
@apply my-6 border-l-4 border-primary/40 bg-secondary/5 py-2 pl-4 italic;
|
||||||
|
}
|
||||||
|
.prose-content table {
|
||||||
|
@apply my-6 w-full border-collapse text-left text-sm;
|
||||||
|
}
|
||||||
|
.prose-content th {
|
||||||
|
@apply border-b-2 border-secondary/20 bg-secondary/5 px-3 py-2 font-semibold;
|
||||||
|
}
|
||||||
|
.prose-content td {
|
||||||
|
@apply border-b border-secondary/10 px-3 py-2 align-top;
|
||||||
|
}
|
||||||
|
.prose-content code {
|
||||||
|
@apply rounded bg-secondary/10 px-1.5 py-0.5 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data tables (payment table, etc.) ---------------------------------- */
|
||||||
|
.data-table {
|
||||||
|
@apply w-full border-collapse overflow-hidden text-left text-sm;
|
||||||
|
}
|
||||||
|
.data-table th {
|
||||||
|
@apply border-b-2 border-secondary/20 bg-secondary/5 px-3 py-2 font-semibold;
|
||||||
|
}
|
||||||
|
.data-table td {
|
||||||
|
@apply border-b border-secondary/10 px-3 py-2 align-middle;
|
||||||
|
}
|
||||||
|
.data-table tr:hover td {
|
||||||
|
@apply bg-primary/5;
|
||||||
|
}
|
||||||
|
.data-table tr.is-active-locale td {
|
||||||
|
@apply bg-primary/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav ---------------------------------------------------------------- */
|
||||||
|
.nav-link {
|
||||||
|
@apply rounded-md px-3 py-2 text-sm font-medium text-secondary/80 no-underline hover:bg-secondary/5 hover:text-secondary;
|
||||||
|
}
|
||||||
|
.nav-link-active {
|
||||||
|
@apply bg-primary/10 text-primary;
|
||||||
|
}
|
||||||
|
/* Desktop nav can scroll horizontally when there are many sections; hide the
|
||||||
|
scrollbar chrome so it stays clean (still scrollable via trackpad/shift-wheel). */
|
||||||
|
.nav-scroll {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.nav-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Overlays: age gate + cookie consent -------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.age-gate {
|
||||||
|
@apply fixed inset-0 z-[100] hidden items-center justify-center bg-secondary/95 p-4 text-center;
|
||||||
|
}
|
||||||
|
.age-gate-active .age-gate {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
.age-gate-card {
|
||||||
|
@apply w-full max-w-md rounded-2xl bg-white p-8 shadow-2xl;
|
||||||
|
}
|
||||||
|
.cookie-banner {
|
||||||
|
@apply fixed inset-x-0 bottom-0 z-[90] border-t-2 border-primary/30 bg-white p-4 shadow-2xl sm:p-6;
|
||||||
|
}
|
||||||
|
.cookie-toggle {
|
||||||
|
@apply flex items-start gap-3 rounded-lg border border-secondary/10 p-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Utilities layer ---------------------------------------------------- */
|
||||||
|
@layer utilities {
|
||||||
|
/* Lock page scroll while the age gate is open. */
|
||||||
|
.age-gate-active {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hero-gradient {
|
||||||
|
background-image: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
|
||||||
|
}
|
||||||
|
/* Placeholder used when an image resource is missing (guarded pipeline). */
|
||||||
|
.img-placeholder {
|
||||||
|
background-image: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* =========================================================================
|
||||||
|
tailwind.src.css — template-free entry used ONLY by `npm run build:css`
|
||||||
|
to emit a standalone, verifiable Tailwind stylesheet (dist/).
|
||||||
|
The site itself is compiled from main.css via Hugo Pipes (css.PostCSS),
|
||||||
|
which additionally injects the data/brand.yaml :root tokens.
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,175 @@
|
|||||||
|
/* =========================================================================
|
||||||
|
age-gate.js — non-dismissible age verification interstitial.
|
||||||
|
The <html> class "age-gate-active" is set as early as possible by an inline
|
||||||
|
script in baseof.html to avoid a flash for returning (confirmed) visitors.
|
||||||
|
This module wires the confirm/exit buttons, syncs the displayed age with the
|
||||||
|
active locale (set by the locale switcher) and — because the dialog is
|
||||||
|
role="dialog" aria-modal="true" — manages focus: it moves focus into the
|
||||||
|
dialog, traps Tab/Shift+Tab, marks the rest of the page inert while active,
|
||||||
|
and restores focus on confirm.
|
||||||
|
|
||||||
|
Deliberately dependency-free (no Alpine, no eval/new Function): the gate is
|
||||||
|
driven purely by the CSS "age-gate-active" class, so it still fails safe
|
||||||
|
(gate shows, confirm works) even if Alpine never loads.
|
||||||
|
========================================================================= */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var STORAGE_KEY = "ageConfirmed";
|
||||||
|
var INERT_SELECTORS = ["header", "main", "footer"];
|
||||||
|
var FOCUSABLE =
|
||||||
|
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
|
||||||
|
|
||||||
|
var gate = null;
|
||||||
|
var lastFocused = null;
|
||||||
|
var trapActive = false;
|
||||||
|
|
||||||
|
function hasConfirmed() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(STORAGE_KEY) === "1";
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusableIn(el) {
|
||||||
|
return Array.prototype.filter.call(
|
||||||
|
el.querySelectorAll(FOCUSABLE),
|
||||||
|
function (node) {
|
||||||
|
return (
|
||||||
|
node.offsetWidth > 0 ||
|
||||||
|
node.offsetHeight > 0 ||
|
||||||
|
node === document.activeElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (!trapActive || e.key !== "Tab" || !gate) return;
|
||||||
|
var f = focusableIn(gate);
|
||||||
|
if (!f.length) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var first = f[0];
|
||||||
|
var last = f[f.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocusIn(e) {
|
||||||
|
if (trapActive && gate && !gate.contains(e.target)) {
|
||||||
|
var f = focusableIn(gate);
|
||||||
|
if (f.length) f[0].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBackgroundInert(on) {
|
||||||
|
INERT_SELECTORS.forEach(function (sel) {
|
||||||
|
var el = document.querySelector(sel);
|
||||||
|
if (!el) return;
|
||||||
|
if (on) {
|
||||||
|
el.setAttribute("inert", "");
|
||||||
|
el.setAttribute("aria-hidden", "true");
|
||||||
|
} else {
|
||||||
|
el.removeAttribute("inert");
|
||||||
|
el.removeAttribute("aria-hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTrap() {
|
||||||
|
if (trapActive || !gate) return;
|
||||||
|
trapActive = true;
|
||||||
|
lastFocused = document.activeElement;
|
||||||
|
setBackgroundInert(true);
|
||||||
|
document.addEventListener("keydown", onKeydown, true);
|
||||||
|
document.addEventListener("focusin", onFocusIn);
|
||||||
|
var confirmBtn = gate.querySelector("[data-age-confirm]");
|
||||||
|
if (confirmBtn) {
|
||||||
|
// Defer to ensure the element is focusable after the first paint.
|
||||||
|
window.setTimeout(function () {
|
||||||
|
confirmBtn.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseTrap() {
|
||||||
|
trapActive = false;
|
||||||
|
setBackgroundInert(false);
|
||||||
|
document.removeEventListener("keydown", onKeydown, true);
|
||||||
|
document.removeEventListener("focusin", onFocusIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFocus() {
|
||||||
|
var target =
|
||||||
|
(lastFocused &&
|
||||||
|
document.contains(lastFocused) &&
|
||||||
|
lastFocused !== document.body &&
|
||||||
|
lastFocused) ||
|
||||||
|
document.querySelector("header a, header button") ||
|
||||||
|
document.getElementById("main");
|
||||||
|
if (!target) return;
|
||||||
|
if (target.id === "main" && !target.hasAttribute("tabindex")) {
|
||||||
|
target.setAttribute("tabindex", "-1");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
target.focus();
|
||||||
|
} catch (e) {
|
||||||
|
/* focus can fail on detached nodes — ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAge() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, "1");
|
||||||
|
} catch (e) {
|
||||||
|
/* storage unavailable — still allow through for this session */
|
||||||
|
}
|
||||||
|
document.documentElement.classList.remove("age-gate-active");
|
||||||
|
releaseTrap();
|
||||||
|
restoreFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitSite() {
|
||||||
|
window.location.href = "https://www.google.com/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
gate = document.getElementById("age-gate");
|
||||||
|
if (!gate) {
|
||||||
|
document.documentElement.classList.remove("age-gate-active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmBtn = gate.querySelector("[data-age-confirm]");
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener("click", confirmAge);
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitBtn = gate.querySelector("[data-age-exit]");
|
||||||
|
if (exitBtn) {
|
||||||
|
exitBtn.addEventListener("click", exitSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasConfirmed()) {
|
||||||
|
document.documentElement.classList.remove("age-gate-active");
|
||||||
|
setBackgroundInert(false);
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add("age-gate-active");
|
||||||
|
activateTrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/* =========================================================================
|
||||||
|
cookie-consent.js — GDPR-style consent manager.
|
||||||
|
Non-essential scripts must be added to the page as:
|
||||||
|
<script type="text/plain" data-cookiecategory="analytics"> ... </script>
|
||||||
|
They stay inert until the visitor grants that category, at which point this
|
||||||
|
module reactivates them. Consent is stored in localStorage.
|
||||||
|
========================================================================= */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var STORAGE_KEY = "cookieConsent";
|
||||||
|
var CATEGORIES = ["necessary", "analytics", "marketing"];
|
||||||
|
|
||||||
|
function readConsent() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConsent(consent) {
|
||||||
|
consent.necessary = true; // always on
|
||||||
|
consent.ts = Date.now();
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(consent));
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
activateScripts(consent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateScripts(consent) {
|
||||||
|
var nodes = document.querySelectorAll(
|
||||||
|
'script[type="text/plain"][data-cookiecategory]'
|
||||||
|
);
|
||||||
|
Array.prototype.forEach.call(nodes, function (node) {
|
||||||
|
if (node.getAttribute("data-cookie-activated") === "1") return;
|
||||||
|
var cat = node.getAttribute("data-cookiecategory");
|
||||||
|
if (!consent[cat]) return;
|
||||||
|
|
||||||
|
var s = document.createElement("script");
|
||||||
|
for (var i = 0; i < node.attributes.length; i++) {
|
||||||
|
var attr = node.attributes[i];
|
||||||
|
if (attr.name === "type" || attr.name === "data-cookiecategory") continue;
|
||||||
|
s.setAttribute(attr.name, attr.value);
|
||||||
|
}
|
||||||
|
s.type = "text/javascript";
|
||||||
|
if (!node.src) {
|
||||||
|
s.text = node.textContent;
|
||||||
|
}
|
||||||
|
node.parentNode.insertBefore(s, node.nextSibling);
|
||||||
|
node.setAttribute("data-cookie-activated", "1");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function banner() {
|
||||||
|
return document.getElementById("cookie-consent");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBanner() {
|
||||||
|
var b = banner();
|
||||||
|
if (b) b.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideBanner() {
|
||||||
|
var b = banner();
|
||||||
|
if (b) b.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceptAll() {
|
||||||
|
var consent = { necessary: true };
|
||||||
|
CATEGORIES.forEach(function (c) {
|
||||||
|
consent[c] = true;
|
||||||
|
});
|
||||||
|
writeConsent(consent);
|
||||||
|
hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectNonEssential() {
|
||||||
|
var consent = { necessary: true, analytics: false, marketing: false };
|
||||||
|
writeConsent(consent);
|
||||||
|
hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreferences() {
|
||||||
|
var b = banner();
|
||||||
|
var consent = { necessary: true };
|
||||||
|
CATEGORIES.forEach(function (c) {
|
||||||
|
if (c === "necessary") return;
|
||||||
|
var box = b
|
||||||
|
? b.querySelector('[data-cc-category="' + c + '"]')
|
||||||
|
: null;
|
||||||
|
consent[c] = !!(box && box.checked);
|
||||||
|
});
|
||||||
|
writeConsent(consent);
|
||||||
|
hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleManage() {
|
||||||
|
var b = banner();
|
||||||
|
if (!b) return;
|
||||||
|
var panel = b.querySelector("[data-cc-panel]");
|
||||||
|
if (panel) panel.hidden = !panel.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var b = banner();
|
||||||
|
var existing = readConsent();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
activateScripts(existing);
|
||||||
|
if (b) b.hidden = true;
|
||||||
|
} else if (b) {
|
||||||
|
b.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!b) return;
|
||||||
|
|
||||||
|
var accept = b.querySelector("[data-cc-accept-all]");
|
||||||
|
if (accept) accept.addEventListener("click", acceptAll);
|
||||||
|
|
||||||
|
var reject = b.querySelector("[data-cc-reject]");
|
||||||
|
if (reject) reject.addEventListener("click", rejectNonEssential);
|
||||||
|
|
||||||
|
var save = b.querySelector("[data-cc-save]");
|
||||||
|
if (save) save.addEventListener("click", savePreferences);
|
||||||
|
|
||||||
|
var manage = b.querySelector("[data-cc-manage]");
|
||||||
|
if (manage) manage.addEventListener("click", toggleManage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: reopen the consent banner (used by footer / cookie-policy links).
|
||||||
|
window.openCookieSettings = function () {
|
||||||
|
var existing = readConsent();
|
||||||
|
var b = banner();
|
||||||
|
if (b && existing) {
|
||||||
|
CATEGORIES.forEach(function (c) {
|
||||||
|
if (c === "necessary") return;
|
||||||
|
var box = b.querySelector('[data-cc-category="' + c + '"]');
|
||||||
|
if (box) box.checked = !!existing[c];
|
||||||
|
});
|
||||||
|
var panel = b.querySelector("[data-cc-panel]");
|
||||||
|
if (panel) panel.hidden = false;
|
||||||
|
}
|
||||||
|
showBanner();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/* =========================================================================
|
||||||
|
games-filter.js — Alpine.js component powering the slots library filter.
|
||||||
|
Cards are rendered server-side by Hugo (so thumbnails go through the image
|
||||||
|
pipeline); this component toggles their visibility via data-attributes.
|
||||||
|
Registered on `alpine:init`, so this file must load before the Alpine CDN.
|
||||||
|
========================================================================= */
|
||||||
|
document.addEventListener("alpine:init", function () {
|
||||||
|
window.Alpine.data("gamesFilter", function () {
|
||||||
|
return {
|
||||||
|
provider: "all",
|
||||||
|
category: "all",
|
||||||
|
search: "",
|
||||||
|
visibleCount: 0,
|
||||||
|
|
||||||
|
get normalizedSearch() {
|
||||||
|
return this.search.trim().toLowerCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
cardVisible: function (el) {
|
||||||
|
var okProvider =
|
||||||
|
this.provider === "all" || el.dataset.provider === this.provider;
|
||||||
|
var okCategory =
|
||||||
|
this.category === "all" || el.dataset.category === this.category;
|
||||||
|
var okSearch =
|
||||||
|
this.normalizedSearch === "" ||
|
||||||
|
(el.dataset.title || "").indexOf(this.normalizedSearch) !== -1;
|
||||||
|
return okProvider && okCategory && okSearch;
|
||||||
|
},
|
||||||
|
|
||||||
|
recount: function () {
|
||||||
|
var self = this;
|
||||||
|
var cards = this.$root.querySelectorAll("[data-game]");
|
||||||
|
var n = 0;
|
||||||
|
Array.prototype.forEach.call(cards, function (el) {
|
||||||
|
if (self.cardVisible(el)) n++;
|
||||||
|
});
|
||||||
|
this.visibleCount = n;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: function () {
|
||||||
|
this.provider = "all";
|
||||||
|
this.category = "all";
|
||||||
|
this.search = "";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# locales.yaml — per-market legal/compliance data layer.
|
||||||
|
# One entry per brand.supportedLocales. Consumed by the client-side locale
|
||||||
|
# switcher, the age gate and the responsible-gambling block.
|
||||||
|
#
|
||||||
|
# NOTE: Responsible-gambling org names/URLs below are the best-known public
|
||||||
|
# support services per market. Verify them against current regulation before
|
||||||
|
# go-live; entries marked "(placeholder)" MUST be replaced.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
- locale: "en-GB"
|
||||||
|
label: "United Kingdom"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "BeGambleAware / GamCare"
|
||||||
|
responsibleGamblingUrl: "https://www.begambleaware.org"
|
||||||
|
currencySymbol: "£"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Passport, driving licence or national ID card"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "de-DE"
|
||||||
|
label: "Germany"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Check dein Spiel (BZgA)"
|
||||||
|
responsibleGamblingUrl: "https://www.check-dein-spiel.de"
|
||||||
|
currencySymbol: "€"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Personalausweis or passport"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "de-AT"
|
||||||
|
label: "Austria"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Spielsuchthilfe (Spielerschutz)"
|
||||||
|
responsibleGamblingUrl: "https://www.spielsuchthilfe.at"
|
||||||
|
currencySymbol: "€"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Reisepass or national ID card"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "it-IT"
|
||||||
|
label: "Italy"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Gioca Responsabile"
|
||||||
|
responsibleGamblingUrl: "https://www.giocaresponsabile.it"
|
||||||
|
currencySymbol: "€"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Carta d'identità or passport"
|
||||||
|
- "Codice fiscale"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "es-ES"
|
||||||
|
label: "Spain"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Jugar Bien"
|
||||||
|
responsibleGamblingUrl: "https://www.jugarbien.es"
|
||||||
|
currencySymbol: "€"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "DNI, NIE or passport"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "pl-PL"
|
||||||
|
label: "Poland"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Anonimowi Hazardzisci"
|
||||||
|
responsibleGamblingUrl: "https://anonimowihazardzisci.org"
|
||||||
|
currencySymbol: "zl"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Dowod osobisty (national ID) or passport"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "nl-NL"
|
||||||
|
label: "Netherlands"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Loket Kansspel"
|
||||||
|
responsibleGamblingUrl: "https://www.loketkansspel.nl"
|
||||||
|
currencySymbol: "€"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Paspoort, ID card or driving licence"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "fi-FI"
|
||||||
|
label: "Finland"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Peluuri"
|
||||||
|
responsibleGamblingUrl: "https://www.peluuri.fi"
|
||||||
|
currencySymbol: "€"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Passport or national ID card"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "sv-SE"
|
||||||
|
label: "Sweden"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Stodlinjen"
|
||||||
|
responsibleGamblingUrl: "https://www.stodlinjen.se"
|
||||||
|
currencySymbol: "kr"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "BankID verification"
|
||||||
|
- "Passport or national ID card"
|
||||||
|
|
||||||
|
- locale: "no-NO"
|
||||||
|
label: "Norway"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "Hjelpelinjen"
|
||||||
|
responsibleGamblingUrl: "https://hjelpelinjen.no"
|
||||||
|
currencySymbol: "kr"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "Passport or national ID card"
|
||||||
|
- "Proof of address (utility bill or bank statement, < 3 months)"
|
||||||
|
|
||||||
|
- locale: "da-DK"
|
||||||
|
label: "Denmark"
|
||||||
|
ageRestriction: 18
|
||||||
|
responsibleGamblingOrgName: "StopSpillet"
|
||||||
|
responsibleGamblingUrl: "https://www.stopspillet.dk"
|
||||||
|
currencySymbol: "kr"
|
||||||
|
requiredKycDocs:
|
||||||
|
- "MitID verification"
|
||||||
|
- "Passport or national ID card"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# payment_methods.yaml — rendered by the {{< payment-table >}} shortcut and the
|
||||||
|
# partials/payment-table.html partial. `availableLocales` powers per-market
|
||||||
|
# filtering. Amounts are in the market's major currency unit.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
- name: "Visa"
|
||||||
|
type: "card"
|
||||||
|
minDeposit: 10
|
||||||
|
maxDeposit: 5000
|
||||||
|
processingTime: "Instant"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/visa.svg"
|
||||||
|
availableLocales: ["en-GB", "de-DE", "de-AT", "it-IT", "es-ES", "pl-PL", "nl-NL", "fi-FI", "sv-SE", "no-NO", "da-DK"]
|
||||||
|
|
||||||
|
- name: "Mastercard"
|
||||||
|
type: "card"
|
||||||
|
minDeposit: 10
|
||||||
|
maxDeposit: 5000
|
||||||
|
processingTime: "Instant"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/mastercard.svg"
|
||||||
|
availableLocales: ["en-GB", "de-DE", "de-AT", "it-IT", "es-ES", "pl-PL", "nl-NL", "fi-FI", "sv-SE", "no-NO", "da-DK"]
|
||||||
|
|
||||||
|
- name: "PayPal"
|
||||||
|
type: "e-wallet"
|
||||||
|
minDeposit: 10
|
||||||
|
maxDeposit: 8000
|
||||||
|
processingTime: "Instant"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/paypal.svg"
|
||||||
|
availableLocales: ["en-GB", "de-DE", "de-AT", "it-IT", "es-ES"]
|
||||||
|
|
||||||
|
- name: "Skrill"
|
||||||
|
type: "e-wallet"
|
||||||
|
minDeposit: 10
|
||||||
|
maxDeposit: 10000
|
||||||
|
processingTime: "Instant"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/skrill.svg"
|
||||||
|
availableLocales: ["en-GB", "de-DE", "de-AT", "it-IT", "es-ES", "pl-PL", "nl-NL", "fi-FI"]
|
||||||
|
|
||||||
|
- name: "Neteller"
|
||||||
|
type: "e-wallet"
|
||||||
|
minDeposit: 10
|
||||||
|
maxDeposit: 10000
|
||||||
|
processingTime: "Instant"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/neteller.svg"
|
||||||
|
availableLocales: ["en-GB", "de-DE", "it-IT", "es-ES", "pl-PL", "fi-FI", "sv-SE"]
|
||||||
|
|
||||||
|
- name: "Trustly"
|
||||||
|
type: "bank-transfer"
|
||||||
|
minDeposit: 20
|
||||||
|
maxDeposit: 20000
|
||||||
|
processingTime: "Instant (deposit) / 1-2 days (withdrawal)"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/trustly.svg"
|
||||||
|
availableLocales: ["de-DE", "de-AT", "nl-NL", "fi-FI", "sv-SE", "no-NO", "da-DK"]
|
||||||
|
|
||||||
|
- name: "Paysafecard"
|
||||||
|
type: "prepaid"
|
||||||
|
minDeposit: 10
|
||||||
|
maxDeposit: 1000
|
||||||
|
processingTime: "Instant (deposit only)"
|
||||||
|
fees: "None"
|
||||||
|
logoPath: "/images/payments/paysafecard.svg"
|
||||||
|
availableLocales: ["en-GB", "de-DE", "de-AT", "it-IT", "es-ES", "pl-PL", "nl-NL"]
|
||||||
|
|
||||||
|
- name: "Bitcoin"
|
||||||
|
type: "crypto"
|
||||||
|
minDeposit: 20
|
||||||
|
maxDeposit: 50000
|
||||||
|
processingTime: "10-30 minutes (network dependent)"
|
||||||
|
fees: "Network fee only"
|
||||||
|
logoPath: "/images/payments/bitcoin.svg"
|
||||||
|
availableLocales: ["de-DE", "de-AT", "fi-FI", "no-NO"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# i18n/en.yaml — UI string catalogue (English).
|
||||||
|
# The site currently runs single-language via the data layer, but externalising
|
||||||
|
# UI strings here means Hugo multilingual can be enabled later by adding
|
||||||
|
# [languages] to hugo.toml and translating this file (e.g. i18n/de.yaml).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Global / chrome
|
||||||
|
skipToContent: "Skip to content"
|
||||||
|
playNow: "Play Now"
|
||||||
|
visitCasino: "Visit Casino"
|
||||||
|
readMore: "Read more"
|
||||||
|
selectRegion: "Select your region"
|
||||||
|
toggleMenu: "Toggle navigation menu"
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
footerExplore: "Explore"
|
||||||
|
footerLegal: "Legal & Safety"
|
||||||
|
footerResponsible: "Play Responsibly"
|
||||||
|
cookieSettings: "Cookie settings"
|
||||||
|
affiliateDisclosure: "Affiliate disclosure"
|
||||||
|
allRightsReserved: "All rights reserved."
|
||||||
|
|
||||||
|
# Age gate
|
||||||
|
ageGateTitle: "Please confirm your age"
|
||||||
|
ageGateConfirm: "I confirm I am of legal age"
|
||||||
|
ageGateExit: "I am under age — leave"
|
||||||
|
|
||||||
|
# Cookie consent
|
||||||
|
cookieTitle: "We value your privacy"
|
||||||
|
cookieAcceptAll: "Accept all"
|
||||||
|
cookieReject: "Reject non-essential"
|
||||||
|
cookieManage: "Manage"
|
||||||
|
cookieSave: "Save preferences"
|
||||||
|
cookieNecessary: "Necessary"
|
||||||
|
cookieAnalytics: "Analytics"
|
||||||
|
cookieMarketing: "Marketing"
|
||||||
|
|
||||||
|
# Games filter
|
||||||
|
filterSearch: "Search"
|
||||||
|
filterProvider: "Provider"
|
||||||
|
filterCategory: "Category"
|
||||||
|
filterAllProviders: "All providers"
|
||||||
|
filterAllCategories: "All categories"
|
||||||
|
filterReset: "Reset filters"
|
||||||
|
|
||||||
|
# Responsible gambling
|
||||||
|
rgNeedHelp: "Need help?"
|
||||||
|
rgFindSupport: "Find local support"
|
||||||
|
|
||||||
|
# Common
|
||||||
|
relatedPages: "Related pages"
|
||||||
|
lastUpdated: "Last updated"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<section class="site-container section text-center">
|
||||||
|
<p class="eyebrow">Error 404</p>
|
||||||
|
<h1 class="mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">Page not found</h1>
|
||||||
|
<p class="mx-auto mt-4 max-w-md text-secondary/70">
|
||||||
|
The page you’re looking for doesn’t exist or may have moved. Let’s get you back on track.
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex flex-wrap justify-center gap-3">
|
||||||
|
<a href="{{ "/" | relURL }}" class="btn-primary">Back to home</a>
|
||||||
|
{{ with site.GetPage "faq" }}<a href="{{ .RelPermalink }}" class="btn-secondary">Visit the FAQ</a>{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto mt-12 grid max-w-2xl gap-3 sm:grid-cols-3">
|
||||||
|
{{- range slice "slots" "bonuses" "responsible-gambling" -}}
|
||||||
|
{{ with site.GetPage . }}
|
||||||
|
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline text-left">
|
||||||
|
<span class="font-semibold text-primary">{{ .LinkTitle }}</span>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ .Site.LanguageCode | default "en" }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="{{ .Site.Data.brand.primaryColor }}">
|
||||||
|
|
||||||
|
{{ partial "head-seo.html" . }}
|
||||||
|
|
||||||
|
{{/* Brand + locale data exposed to client-side scripts (age gate, locale switcher).
|
||||||
|
safeJS prevents html/template from escaping the JSON into a quoted string. */}}
|
||||||
|
<script>
|
||||||
|
window.__BRAND__ = {{ dict "name" .Site.Data.brand.name "defaultLocale" .Site.Data.brand.defaultLocale "ageRestrictionDefault" .Site.Data.brand.ageRestrictionDefault | jsonify | safeJS }};
|
||||||
|
window.__LOCALES__ = {{ .Site.Data.locales | jsonify | safeJS }};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{/* Add the age-gate class as early as possible to avoid a flash for confirmed visitors. */}}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
if (localStorage.getItem("ageConfirmed") !== "1") {
|
||||||
|
document.documentElement.classList.add("age-gate-active");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.documentElement.classList.add("age-gate-active");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{/* CSS pipeline: inject brand vars (ExecuteAsTemplate) -> Tailwind/PostCSS ->
|
||||||
|
minify (production only) -> fingerprint. Hugo's [minify] does not minify
|
||||||
|
Pipes resources, so we minify explicitly for production builds. */}}
|
||||||
|
{{ $css := resources.Get "css/main.css" | resources.ExecuteAsTemplate "css/main.css" . | css.PostCSS }}
|
||||||
|
{{ if hugo.IsProduction }}{{ $css = $css | minify }}{{ end }}
|
||||||
|
{{ $css = $css | fingerprint "sha256" }}
|
||||||
|
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ "favicon.svg" | relURL }}" type="image/svg+xml">
|
||||||
|
</head>
|
||||||
|
<body class="flex min-h-screen flex-col bg-white">
|
||||||
|
<a href="#main" class="sr-only rounded bg-primary px-4 py-2 text-primary-contrast focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[110]">{{ i18n "skipToContent" }}</a>
|
||||||
|
|
||||||
|
{{ partial "age-gate-banner.html" . }}
|
||||||
|
{{ partial "header.html" . }}
|
||||||
|
|
||||||
|
<main id="main" class="flex-1">
|
||||||
|
{{ block "main" . }}{{ end }}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ partial "footer.html" . }}
|
||||||
|
{{ partial "cookie-consent.html" . }}
|
||||||
|
|
||||||
|
{{/* App bundle (age gate, cookie consent, games filter). Loads BEFORE Alpine so the
|
||||||
|
gamesFilter component is registered on the alpine:init event. */}}
|
||||||
|
{{ $appJs := slice (resources.Get "js/age-gate.js") (resources.Get "js/cookie-consent.js") (resources.Get "js/games-filter.js") | resources.Concat "js/app.js" | minify | fingerprint "sha256" }}
|
||||||
|
<script defer src="{{ $appJs.RelPermalink }}" integrity="{{ $appJs.Data.Integrity }}" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
{{/* Alpine.js — vendored first-party (assets/js/vendor/alpine.js, pinned 3.14.8,
|
||||||
|
refreshed from node_modules by `npm run build:js`) and fingerprinted so it
|
||||||
|
ships with Subresource Integrity like the other assets. Deferred and placed
|
||||||
|
AFTER the app bundle so components register on alpine:init before core runs. */}}
|
||||||
|
{{ $alpine := resources.Get "js/vendor/alpine.js" | fingerprint "sha256" }}
|
||||||
|
<script defer src="{{ $alpine.RelPermalink }}" integrity="{{ $alpine.Data.Integrity }}" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
{{ partial "breadcrumbs.html" . }}
|
||||||
|
|
||||||
|
<div class="site-container section">
|
||||||
|
<header class="mb-8 max-w-3xl">
|
||||||
|
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">{{ .Params.h1 | default .Title }}</h1>
|
||||||
|
{{ with .Params.description }}<p class="mt-3 text-lg text-secondary/70">{{ . }}</p>{{ end }}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{ with .Content }}<div class="prose-content max-w-3xl">{{ . }}</div>{{ end }}
|
||||||
|
|
||||||
|
{{ with .Pages }}
|
||||||
|
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{{- range . -}}
|
||||||
|
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline">
|
||||||
|
<h2 class="text-lg font-bold">{{ .LinkTitle }}</h2>
|
||||||
|
<p class="mt-2 text-sm text-secondary/70">{{ .Params.description | default (.Summary | plainify) | truncate 120 }}</p>
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ partial "related-pages.html" . }}
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
{{ partial "breadcrumbs.html" . }}
|
||||||
|
|
||||||
|
<article class="site-container section">
|
||||||
|
<header class="mb-8 max-w-3xl">
|
||||||
|
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">{{ .Params.h1 | default .Title }}</h1>
|
||||||
|
{{ with .Params.description }}<p class="mt-3 text-lg text-secondary/70">{{ . }}</p>{{ end }}
|
||||||
|
{{ if not .Lastmod.IsZero }}<p class="mt-2 text-xs text-secondary/70">Last updated {{ .Lastmod.Format "2 January 2006" }}</p>{{ end }}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{ with .Content }}<div class="prose-content max-w-3xl">{{ . }}</div>{{ end }}
|
||||||
|
|
||||||
|
{{ if or .Params.pros .Params.cons }}
|
||||||
|
<section class="mt-12 max-w-3xl">
|
||||||
|
<h2 class="mb-4 text-2xl font-bold">Pros & cons at a glance</h2>
|
||||||
|
{{ partial "pros-cons-table.html" . }}
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Params.showRatingWidget }}
|
||||||
|
<section class="mt-12 max-w-3xl">
|
||||||
|
<h2 class="mb-4 text-2xl font-bold">Our verdict</h2>
|
||||||
|
{{ partial "rating-widget.html" . }}
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Params.showKycByLocale }}
|
||||||
|
<section class="mt-12 max-w-4xl">
|
||||||
|
<h2 class="mb-3 text-2xl font-bold">Verification documents by region</h2>
|
||||||
|
<p class="mb-4 text-secondary/70">KYC requirements vary by market. Choose your region in the header to highlight your row; the full list is below.</p>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th scope="col">Region</th><th scope="col">Required documents (KYC)</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{{- range .Site.Data.locales -}}
|
||||||
|
<tr data-kyc-row data-locale="{{ .locale }}">
|
||||||
|
<th scope="row" class="font-medium">{{ .label | default .locale }}</th>
|
||||||
|
<td><ul class="list-disc space-y-1 pl-4">{{ range .requiredKycDocs }}<li>{{ . }}</li>{{ end }}</ul></td>
|
||||||
|
</tr>
|
||||||
|
{{- end -}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Params.showFaqAccordion }}
|
||||||
|
<section class="mt-12">
|
||||||
|
<h2 class="mb-6 text-2xl font-bold">Frequently asked questions</h2>
|
||||||
|
{{ partial "faq-accordion.html" . }}
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Params.responsibleGamblingSection }}
|
||||||
|
<section class="mt-12">
|
||||||
|
{{ partial "responsible-gambling-block.html" (dict "page" . "variant" "full") }}
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if not .Params.hideCta }}
|
||||||
|
<div class="mt-12 max-w-3xl rounded-2xl border border-primary/20 bg-primary/5 p-6">
|
||||||
|
<h2 class="text-xl font-bold">Ready to get started?</h2>
|
||||||
|
<p class="mt-1 text-secondary/70"><span data-age-value>{{ .Site.Data.brand.ageRestrictionDefault }}</span>+ only. T&Cs apply. Please gamble responsibly.</p>
|
||||||
|
<div class="mt-4">{{ partial "cta-button.html" (dict "page" . "text" "Visit Casino" "size" "lg") }}</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{{ partial "related-pages.html" . }}
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
{{ $brand := .Site.Data.brand }}
|
||||||
|
|
||||||
|
{{/* Hero */}}
|
||||||
|
<section class="hero-gradient text-white">
|
||||||
|
<div class="site-container grid items-center gap-10 py-16 lg:grid-cols-2 lg:py-24">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow text-white/80">{{ $brand.name }}</p>
|
||||||
|
<h1 class="mt-2 text-4xl font-extrabold leading-tight tracking-tight sm:text-5xl">{{ .Params.h1 | default .Title }}</h1>
|
||||||
|
<p class="mt-4 max-w-xl text-lg text-white/90">{{ .Params.description | default $brand.tagline }}</p>
|
||||||
|
<div class="mt-8 flex flex-wrap gap-3">
|
||||||
|
{{ partial "cta-button.html" (dict "page" . "text" "Play Now" "size" "lg") }}
|
||||||
|
{{ with site.GetPage "bonuses" }}<a href="{{ .RelPermalink }}" class="btn border-2 border-white/80 text-white hover:bg-white hover:text-secondary">See bonuses</a>{{ end }}
|
||||||
|
</div>
|
||||||
|
<p class="mt-5 text-sm text-white/70"><span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+ only • T&Cs apply • Please gamble responsibly</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
{{ $hero := resources.Get "images/hero.png" }}
|
||||||
|
<div class="overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/20">
|
||||||
|
{{ partial "responsive-image.html" (dict
|
||||||
|
"resource" $hero
|
||||||
|
"alt" (printf "%s highlights" $brand.name)
|
||||||
|
"loading" "eager"
|
||||||
|
"class" "w-full"
|
||||||
|
"widths" (slice 600 900 1200)
|
||||||
|
"sizes" "(min-width:1024px) 50vw, 100vw") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{/* USP blocks */}}
|
||||||
|
{{ with .Params.usps }}
|
||||||
|
<section class="site-container section">
|
||||||
|
<div class="grid gap-6 md:grid-cols-3">
|
||||||
|
{{- range . -}}
|
||||||
|
<div class="card card-hover">
|
||||||
|
<div class="mb-3 grid h-11 w-11 place-items-center rounded-lg bg-primary/10 text-primary">
|
||||||
|
{{- if .icon -}}
|
||||||
|
<span class="text-xl" aria-hidden="true">{{ .icon }}</span>
|
||||||
|
{{- else -}}
|
||||||
|
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-bold">{{ .title }}</h2>
|
||||||
|
<p class="mt-2 text-secondary/70">{{ .text }}</p>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{/* Editorial intro (from _index.md body) */}}
|
||||||
|
{{ with .Content }}
|
||||||
|
<section class="site-container section pt-0">
|
||||||
|
<div class="prose-content max-w-3xl">{{ . }}</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{/* Quick shortcuts */}}
|
||||||
|
<section class="site-container section pt-0">
|
||||||
|
<h2 class="mb-6 text-2xl font-bold">Start here</h2>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
{{- range slice "registration" "slots" "bonuses" -}}
|
||||||
|
{{ with site.GetPage . }}
|
||||||
|
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline">
|
||||||
|
<span class="badge-primary">{{ .LinkTitle }}</span>
|
||||||
|
<p class="mt-2 text-secondary/70">{{ .Params.description | truncate 90 }}</p>
|
||||||
|
<span class="mt-3 inline-block font-semibold text-primary">Explore →</span>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{/* Featured games */}}
|
||||||
|
{{ $featured := where (where .Site.RegularPages "Section" "slots") "Params.featured" true }}
|
||||||
|
{{ with $featured }}
|
||||||
|
<section class="site-container section pt-0">
|
||||||
|
<div class="mb-6 flex items-end justify-between">
|
||||||
|
<h2 class="text-2xl font-bold">Featured games</h2>
|
||||||
|
{{ with site.GetPage "slots" }}<a href="{{ .RelPermalink }}" class="font-semibold text-primary">All games →</a>{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{{- range first 4 . -}}
|
||||||
|
{{ $img := .Resources.GetMatch "thumb.*" }}
|
||||||
|
<article class="card card-hover overflow-hidden p-0">
|
||||||
|
<div class="aspect-[3/2] overflow-hidden bg-secondary/10">
|
||||||
|
{{ partial "responsive-image.html" (dict "resource" $img "alt" .Title "class" "h-full w-full object-cover" "widths" (slice 300 600)) }}
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<span class="badge-primary">{{ .Params.provider }}</span>
|
||||||
|
<h3 class="mt-2 font-bold"><a href="{{ .RelPermalink }}" class="no-underline hover:underline">{{ .Title }}</a></h3>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ partial "related-pages.html" . }}
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{{- /* age-gate-banner.html — full-screen, non-dismissible age interstitial shown
|
||||||
|
on first visit. Visibility is driven by the .age-gate-active class on
|
||||||
|
<html> (set early in baseof.html and cleared by age-gate.js on confirm).
|
||||||
|
The age value is data-driven and updated per locale by the switcher. */ -}}
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
<div id="age-gate" class="age-gate" role="dialog" aria-modal="true" aria-labelledby="age-gate-title" aria-describedby="age-gate-desc">
|
||||||
|
<div class="age-gate-card">
|
||||||
|
<div class="mx-auto mb-4 grid h-16 w-16 place-items-center rounded-full bg-primary/10 text-xl font-extrabold text-primary">
|
||||||
|
<span><span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+</span>
|
||||||
|
</div>
|
||||||
|
<h2 id="age-gate-title" class="text-2xl font-bold text-secondary">{{ i18n "ageGateTitle" }}</h2>
|
||||||
|
<p id="age-gate-desc" class="mt-3 text-secondary/80">
|
||||||
|
This website is intended for users aged <span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+ only.
|
||||||
|
By entering, you confirm you meet the legal gambling age in your jurisdiction. Gamble responsibly.
|
||||||
|
</p>
|
||||||
|
<div class="mt-6 flex flex-col gap-3">
|
||||||
|
<button type="button" data-age-confirm class="btn-primary btn-lg">{{ i18n "ageGateConfirm" }}</button>
|
||||||
|
<button type="button" data-age-exit class="btn-ghost">{{ i18n "ageGateExit" }}</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-secondary/60">
|
||||||
|
Underage gambling is an offence. This is an independent affiliate resource, not the operator.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{{- /* breadcrumbs.html — visible breadcrumb trail + BreadcrumbList JSON-LD on all
|
||||||
|
non-home pages. Built from .Ancestors (reversed) + the current page. */ -}}
|
||||||
|
{{- if not .IsHome -}}
|
||||||
|
{{- $crumbs := slice -}}
|
||||||
|
{{- range .Ancestors.Reverse -}}
|
||||||
|
{{- $crumbs = $crumbs | append . -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $crumbs = $crumbs | append . -}}
|
||||||
|
{{- $last := sub (len $crumbs) 1 -}}
|
||||||
|
|
||||||
|
<nav aria-label="Breadcrumb" class="site-container pt-6">
|
||||||
|
<ol class="flex flex-wrap items-center gap-1 text-sm text-secondary/60">
|
||||||
|
{{- range $i, $c := $crumbs -}}
|
||||||
|
{{- if $i }}<li aria-hidden="true" class="px-1 text-secondary/30">/</li>{{ end -}}
|
||||||
|
<li>
|
||||||
|
{{- if eq $i $last -}}
|
||||||
|
<span aria-current="page" class="font-medium text-secondary/80">{{ $c.LinkTitle }}</span>
|
||||||
|
{{- else -}}
|
||||||
|
<a href="{{ $c.RelPermalink }}" class="no-underline hover:text-primary hover:underline">{{ $c.LinkTitle }}</a>
|
||||||
|
{{- end -}}
|
||||||
|
</li>
|
||||||
|
{{- end -}}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{- $items := slice -}}
|
||||||
|
{{- range $i, $c := $crumbs -}}
|
||||||
|
{{- $items = $items | append (dict "@type" "ListItem" "position" (add $i 1) "name" $c.LinkTitle "item" $c.Permalink) -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $bc := dict "@context" "https://schema.org" "@type" "BreadcrumbList" "itemListElement" $items -}}
|
||||||
|
<script type="application/ld+json">{{ $bc | jsonify | safeJS }}</script>
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{{- /* cookie-consent.html — GDPR consent banner (accept / reject / manage).
|
||||||
|
Non-essential scripts stay inert (type="text/plain" data-cookiecategory)
|
||||||
|
until the visitor consents; cookie-consent.js then activates them. */ -}}
|
||||||
|
<div id="cookie-consent" class="cookie-banner" role="region" aria-label="Cookie consent" hidden>
|
||||||
|
<div class="site-container">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div class="max-w-2xl text-sm text-secondary/80">
|
||||||
|
<p class="font-semibold text-secondary">{{ i18n "cookieTitle" }}</p>
|
||||||
|
<p class="mt-1">
|
||||||
|
We use strictly necessary cookies to run this site and, with your consent, analytics and
|
||||||
|
marketing cookies to improve it. You can change your choice at any time.
|
||||||
|
{{ with site.GetPage "cookie-policy" }}<a href="{{ .RelPermalink }}" class="font-medium text-primary underline">Read our Cookie Policy</a>.{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" data-cc-accept-all class="btn-primary">{{ i18n "cookieAcceptAll" }}</button>
|
||||||
|
<button type="button" data-cc-reject class="btn-secondary">{{ i18n "cookieReject" }}</button>
|
||||||
|
<button type="button" data-cc-manage class="btn-ghost">{{ i18n "cookieManage" }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-cc-panel hidden class="mt-4">
|
||||||
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="cookie-toggle">
|
||||||
|
<input type="checkbox" checked disabled class="mt-1" aria-label="Necessary cookies (always on)">
|
||||||
|
<span><span class="block font-semibold text-secondary">{{ i18n "cookieNecessary" }}</span><span class="text-xs text-secondary/70">Always active. Required for core functionality.</span></span>
|
||||||
|
</div>
|
||||||
|
<label class="cookie-toggle">
|
||||||
|
<input type="checkbox" data-cc-category="analytics" class="mt-1">
|
||||||
|
<span><span class="block font-semibold text-secondary">{{ i18n "cookieAnalytics" }}</span><span class="text-xs text-secondary/70">Anonymous usage statistics.</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="cookie-toggle">
|
||||||
|
<input type="checkbox" data-cc-category="marketing" class="mt-1">
|
||||||
|
<span><span class="block font-semibold text-secondary">{{ i18n "cookieMarketing" }}</span><span class="text-xs text-secondary/70">Personalised offers and campaign measurement.</span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="button" data-cc-save class="btn-primary">{{ i18n "cookieSave" }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* Example gated script — stays inert until the visitor accepts "analytics".
|
||||||
|
Replace the body with your real analytics snippet (e.g. GA4/Plausible). */}}
|
||||||
|
<script type="text/plain" data-cookiecategory="analytics">
|
||||||
|
// console.log("Analytics enabled after consent");
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{{- /* cta-button.html — the ONLY place an affiliate link is constructed.
|
||||||
|
Link = brand.affiliateBaseUrl ? brand.affiliateSubIdParam = <per-page sub-id>.
|
||||||
|
Never hardcode a raw affiliate URL anywhere else.
|
||||||
|
|
||||||
|
Call with a dict:
|
||||||
|
{{ partial "cta-button.html" (dict "page" . "text" "Play Now") }}
|
||||||
|
Optional keys: subid, style ("primary"|"secondary"), size ("lg"), class, block(bool).
|
||||||
|
|
||||||
|
Sub-id resolution (robust when .File is nil for generated pages):
|
||||||
|
explicit subid -> .File.ContentBaseName -> .Type -> "home"
|
||||||
|
ContentBaseName equals the classic BaseFileName for standard pages and,
|
||||||
|
unlike BaseFileName, yields the real slug for page bundles (e.g. games). */ -}}
|
||||||
|
{{- $page := .page -}}
|
||||||
|
{{- $brand := $page.Site.Data.brand -}}
|
||||||
|
{{- $text := .text | default "Visit Casino" -}}
|
||||||
|
{{- $style := .style | default "primary" -}}
|
||||||
|
{{- $size := .size | default "" -}}
|
||||||
|
|
||||||
|
{{- $subid := .subid | default "" -}}
|
||||||
|
{{- if not $subid -}}
|
||||||
|
{{- if $page.IsHome -}}
|
||||||
|
{{- $subid = "home" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- with $page.File -}}{{- $subid = .ContentBaseName -}}{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if not $subid -}}{{- $subid = $page.Type -}}{{- end -}}
|
||||||
|
{{- if or (eq $subid "_index") (eq $subid "") -}}{{- $subid = "home" -}}{{- end -}}
|
||||||
|
|
||||||
|
{{- $href := printf "%s?%s=%s" $brand.affiliateBaseUrl $brand.affiliateSubIdParam $subid -}}
|
||||||
|
|
||||||
|
{{- $classes := "btn-primary" -}}
|
||||||
|
{{- if eq $style "secondary" -}}{{- $classes = "btn-secondary" -}}{{- end -}}
|
||||||
|
{{- if eq $style "ghost" -}}{{- $classes = "btn-ghost" -}}{{- end -}}
|
||||||
|
{{- if $size -}}{{- $classes = printf "%s btn-%s" $classes $size -}}{{- end -}}
|
||||||
|
{{- if .block -}}{{- $classes = printf "%s w-full" $classes -}}{{- end -}}
|
||||||
|
{{- with .class -}}{{- $classes = printf "%s %s" $classes . -}}{{- end -}}
|
||||||
|
|
||||||
|
<a href="{{ $href }}" class="{{ $classes }}" rel="sponsored nofollow noopener" target="_blank" data-affiliate data-subid="{{ $subid }}">{{ $text }}</a>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{{- /* faq-accordion.html — accordion over data/faq.yaml (sorted by weight).
|
||||||
|
Progressive enhancement: each item is a native <details>/<summary>, so
|
||||||
|
every answer is fully readable and keyboard-operable with NO JavaScript.
|
||||||
|
Alpine (when present) layers single-open accordion behaviour on top via
|
||||||
|
x-bind:open + the native `toggle` event — never hiding content with
|
||||||
|
x-cloak. The FAQPage JSON-LD is emitted separately by schema-jsonld.html. */ -}}
|
||||||
|
{{- $faqs := sort .Site.Data.faq "weight" -}}
|
||||||
|
<div class="mx-auto max-w-3xl divide-y divide-secondary/10 rounded-xl border border-secondary/10" x-data="{ open: null }">
|
||||||
|
{{- range $i, $f := $faqs -}}
|
||||||
|
<details class="group"
|
||||||
|
x-bind:open="open === {{ $i }}"
|
||||||
|
@toggle="open = $event.target.open ? {{ $i }} : (open === {{ $i }} ? null : open)">
|
||||||
|
<summary id="faq-btn-{{ $i }}" aria-controls="faq-panel-{{ $i }}"
|
||||||
|
class="flex w-full cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-left text-base font-semibold text-secondary [&::-webkit-details-marker]:hidden">
|
||||||
|
<span>{{ $f.question }}</span>
|
||||||
|
<svg class="h-5 w-5 shrink-0 text-primary transition-transform duration-200 group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div id="faq-panel-{{ $i }}" role="region" aria-labelledby="faq-btn-{{ $i }}"
|
||||||
|
class="px-5 pb-5 text-secondary/80">
|
||||||
|
<p>{{ $f.answer }}</p>
|
||||||
|
{{- with $f.category }}<p class="mt-2 text-xs uppercase tracking-wide text-secondary/70">{{ . }}</p>{{ end -}}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{{- /* footer.html — appears on every page. Includes (per spec): affiliate
|
||||||
|
disclosure link, licence/authority text (from data/brand.yaml),
|
||||||
|
responsible-gambling links, cookie policy link, an "18+ Gamble
|
||||||
|
Responsibly" badge and a dynamic copyright year. */ -}}
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
{{- $footerPages := where .Site.Pages "Params.showInFooter" true -}}
|
||||||
|
{{- $primary := sort (where $footerPages "Params.footerGroup" "primary") "Weight" -}}
|
||||||
|
{{- $legal := sort (where $footerPages "Params.footerGroup" "legal") "Weight" -}}
|
||||||
|
|
||||||
|
<footer class="mt-16 bg-secondary text-white">
|
||||||
|
<div class="site-container grid gap-10 py-12 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
|
||||||
|
{{/* Brand + social */}}
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{{ partial "responsive-image.html" (dict "src" $brand.logoPath "alt" (printf "%s logo" $brand.name) "class" "h-9 w-9") }}
|
||||||
|
<span class="text-lg font-extrabold">{{ $brand.name }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-sm text-white/70">{{ $brand.tagline }}</p>
|
||||||
|
<div class="mt-4 flex items-center gap-3">
|
||||||
|
{{- range $brand.socialLinks -}}
|
||||||
|
<a href="{{ .url }}" target="_blank" rel="noopener nofollow" class="text-white/70 hover:text-white" aria-label="{{ .platform | title }}">
|
||||||
|
{{- if eq .platform "twitter" -}}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.9 2H22l-7.5 8.6L23 22h-6.8l-5.3-7-6.1 7H1.7l8-9.2L1 2h7l4.8 6.4L18.9 2Zm-2.4 18h1.9L7.6 4H5.6l10.9 16Z"/></svg>
|
||||||
|
{{- else if eq .platform "facebook" -}}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22 12a10 10 0 1 0-11.6 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.2c-1.2 0-1.6.8-1.6 1.6V12h2.7l-.4 2.9h-2.3v7A10 10 0 0 0 22 12Z"/></svg>
|
||||||
|
{{- else if eq .platform "instagram" -}}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.2c3.2 0 3.6 0 4.9.1 1.2.1 1.8.3 2.2.4.6.2 1 .5 1.4.9.4.4.7.8.9 1.4.1.4.3 1 .4 2.2.1 1.3.1 1.7.1 4.9s0 3.6-.1 4.9c-.1 1.2-.3 1.8-.4 2.2-.2.6-.5 1-.9 1.4-.4.4-.8.7-1.4.9-.4.1-1 .3-2.2.4-1.3.1-1.7.1-4.9.1s-3.6 0-4.9-.1c-1.2-.1-1.8-.3-2.2-.4-.6-.2-1-.5-1.4-.9-.4-.4-.7-.8-.9-1.4-.1-.4-.3-1-.4-2.2C2.2 15.6 2.2 15.2 2.2 12s0-3.6.1-4.9c.1-1.2.3-1.8.4-2.2.2-.6.5-1 .9-1.4.4-.4.8-.7 1.4-.9.4-.1 1-.3 2.2-.4C8.4 2.2 8.8 2.2 12 2.2Zm0 3.2A6.6 6.6 0 1 0 18.6 12 6.6 6.6 0 0 0 12 5.4Zm0 10.9A4.3 4.3 0 1 1 16.3 12 4.3 4.3 0 0 1 12 16.3Zm6.9-11.1a1.5 1.5 0 1 1-1.5-1.5 1.5 1.5 0 0 1 1.5 1.5Z"/></svg>
|
||||||
|
{{- else if eq .platform "youtube" -}}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.4-4.7a2.5 2.5 0 0 0-1.8-1.8C19.3 5 12 5 12 5s-7.3 0-8.8.5A2.5 2.5 0 0 0 1.4 7.3C1 8.8 1 12 1 12s0 3.2.4 4.7a2.5 2.5 0 0 0 1.8 1.8C4.7 19 12 19 12 19s7.3 0 8.8-.5a2.5 2.5 0 0 0 1.8-1.8C23 15.2 23 12 23 12Zm-13 3V9l5.2 3-5.2 3Z"/></svg>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="text-sm">{{ .platform | title }}</span>
|
||||||
|
{{- end -}}
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* Explore */}}
|
||||||
|
<nav aria-label="Footer">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wider text-white/60">{{ i18n "footerExplore" }}</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
{{- range $primary -}}
|
||||||
|
<li><a href="{{ .RelPermalink }}" class="text-white/80 no-underline hover:text-white hover:underline">{{ .LinkTitle }}</a></li>
|
||||||
|
{{- end -}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{/* Legal & safety */}}
|
||||||
|
<nav aria-label="Legal and safety">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wider text-white/60">{{ i18n "footerLegal" }}</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
{{- range $legal -}}
|
||||||
|
<li><a href="{{ .RelPermalink }}" class="text-white/80 no-underline hover:text-white hover:underline">{{ .LinkTitle }}</a></li>
|
||||||
|
{{- end -}}
|
||||||
|
<li>
|
||||||
|
<button type="button" onclick="window.openCookieSettings && window.openCookieSettings()" class="text-white/80 underline-offset-2 hover:text-white hover:underline">{{ i18n "cookieSettings" }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{/* Responsible gambling summary */}}
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wider text-white/60">{{ i18n "footerResponsible" }}</h2>
|
||||||
|
<div class="mt-3 text-sm text-white/80">
|
||||||
|
{{ partial "responsible-gambling-block.html" (dict "page" . "variant" "compact") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ partial "trust-badges.html" . }}
|
||||||
|
|
||||||
|
{{/* Licence + copyright bar */}}
|
||||||
|
<div class="border-t border-white/10">
|
||||||
|
<div class="site-container flex flex-col gap-3 py-6 text-xs text-white/60 md:flex-row md:items-center md:justify-between">
|
||||||
|
<p>
|
||||||
|
<span class="mr-2 inline-flex items-center rounded bg-white/10 px-2 py-1 font-bold text-white"><span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+ Gamble Responsibly</span>
|
||||||
|
{{ $brand.legalOperatorName }} is licensed and regulated by the {{ $brand.licenseAuthority }} (Licence No. {{ $brand.licenseNumber }}).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
© {{ now.Year }} {{ $brand.name }}. {{ i18n "allRightsReserved" }}
|
||||||
|
{{ with site.GetPage "affiliate-disclosure" }}<a href="{{ .RelPermalink }}" class="ml-2 text-white/80 underline hover:text-white">{{ i18n "affiliateDisclosure" }}</a>{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{{- /* games-filter.html — Alpine.js-powered filter for the slots library.
|
||||||
|
Call with (dict "page" . "games" <slice of game pages>).
|
||||||
|
Cards are rendered server-side (so thumbnails use the image pipeline) and
|
||||||
|
shown/hidden by the gamesFilter Alpine component (assets/js/games-filter.js). */ -}}
|
||||||
|
{{- $games := .games -}}
|
||||||
|
|
||||||
|
{{- $providers := slice -}}
|
||||||
|
{{- range $games -}}{{ with .Params.provider }}{{ $providers = $providers | append . }}{{ end }}{{- end -}}
|
||||||
|
{{- $providers = $providers | uniq | sort -}}
|
||||||
|
|
||||||
|
{{- $categories := slice -}}
|
||||||
|
{{- range $games -}}{{ with .Params.category }}{{ $categories = $categories | append . }}{{ end }}{{- end -}}
|
||||||
|
{{- $categories = $categories | uniq | sort -}}
|
||||||
|
|
||||||
|
<div x-data="gamesFilter()" x-init="recount()" x-effect="recount()">
|
||||||
|
{{/* Controls */}}
|
||||||
|
<div class="grid gap-3 rounded-xl border border-secondary/10 bg-secondary/5 p-4 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label for="game-search" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-secondary/60">{{ i18n "filterSearch" }}</label>
|
||||||
|
<input id="game-search" type="search" x-model="search" placeholder="Game name…"
|
||||||
|
class="w-full rounded-md border border-secondary/20 px-3 py-2 text-sm focus-visible:outline-primary">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="game-provider" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-secondary/60">{{ i18n "filterProvider" }}</label>
|
||||||
|
<select id="game-provider" x-model="provider" class="w-full rounded-md border border-secondary/20 px-3 py-2 text-sm focus-visible:outline-primary">
|
||||||
|
<option value="all">{{ i18n "filterAllProviders" }}</option>
|
||||||
|
{{- range $providers }}<option value="{{ . }}">{{ . }}</option>{{ end -}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="game-category" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-secondary/60">{{ i18n "filterCategory" }}</label>
|
||||||
|
<select id="game-category" x-model="category" class="w-full rounded-md border border-secondary/20 px-3 py-2 text-sm focus-visible:outline-primary">
|
||||||
|
<option value="all">{{ i18n "filterAllCategories" }}</option>
|
||||||
|
{{- range $categories }}<option value="{{ . }}">{{ . | title }}</option>{{ end -}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex items-center justify-between text-sm text-secondary/60">
|
||||||
|
<p><span x-text="visibleCount"></span> game(s) shown</p>
|
||||||
|
<button type="button" @click="reset()" class="font-semibold text-primary hover:underline">{{ i18n "filterReset" }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* Cards */}}
|
||||||
|
<div class="mt-5 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{{- range $games -}}
|
||||||
|
{{- $img := .Resources.GetMatch "thumb.*" -}}
|
||||||
|
<article data-game
|
||||||
|
data-provider="{{ .Params.provider }}"
|
||||||
|
data-category="{{ .Params.category }}"
|
||||||
|
data-title="{{ .Title | lower }}"
|
||||||
|
x-show="cardVisible($el)" x-transition.opacity
|
||||||
|
class="card card-hover overflow-hidden p-0">
|
||||||
|
<div class="aspect-[3/2] w-full overflow-hidden bg-secondary/10">
|
||||||
|
{{ partial "responsive-image.html" (dict
|
||||||
|
"resource" $img
|
||||||
|
"alt" (printf "%s by %s" .Title (.Params.provider | default "provider"))
|
||||||
|
"class" "h-full w-full object-cover"
|
||||||
|
"widths" (slice 300 600)
|
||||||
|
"sizes" "(min-width:1024px) 300px, (min-width:640px) 50vw, 100vw") }}
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span class="badge-primary">{{ .Params.provider }}</span>
|
||||||
|
<span class="badge-muted capitalize">{{ .Params.category }}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-2 text-lg font-bold"><a href="{{ .RelPermalink }}" class="no-underline hover:underline">{{ .Title }}</a></h3>
|
||||||
|
{{ with .Params.rtp }}<p class="mt-1 text-sm text-secondary/60">RTP {{ . }}%</p>{{ end }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p x-show="visibleCount === 0" x-cloak class="mt-6 rounded-lg bg-secondary/5 p-6 text-center text-secondary/70">
|
||||||
|
No games match your filters. <button type="button" @click="reset()" class="font-semibold text-primary underline">Reset</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{{- /* head-seo.html — <title>, meta description, canonical, Open Graph, Twitter,
|
||||||
|
RSS alternate, and JSON-LD (delegated to schema-jsonld.html). All values
|
||||||
|
fall back sensibly to brand data. */ -}}
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
|
||||||
|
{{- $title := "" -}}
|
||||||
|
{{- if .IsHome -}}
|
||||||
|
{{- $title = printf "%s | %s" $brand.name $brand.tagline -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $title = printf "%s | %s" (.Params.title | default .Title) $brand.name -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- $desc := "" -}}
|
||||||
|
{{- with .Params.description -}}
|
||||||
|
{{- $desc = . -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $desc = .Summary | default .Site.Params.defaultDescription -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $desc = $desc | plainify | chomp | truncate 160 -}}
|
||||||
|
|
||||||
|
{{- /* Per-page OG image override (.Params.image) with fallback to the site default. */ -}}
|
||||||
|
{{- $ogImage := (.Params.image | default .Site.Params.ogImage) | absURL -}}
|
||||||
|
|
||||||
|
<title>{{ $title }}</title>
|
||||||
|
<meta name="description" content="{{ $desc }}">
|
||||||
|
<link rel="canonical" href="{{ .Permalink }}">
|
||||||
|
<meta name="robots" content="{{ if .Params.noindex }}noindex,nofollow{{ else }}index,follow{{ end }}">
|
||||||
|
<meta name="author" content="{{ $brand.name }}">
|
||||||
|
|
||||||
|
{{- /* Open Graph */ -}}
|
||||||
|
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
|
||||||
|
<meta property="og:title" content="{{ $title }}">
|
||||||
|
<meta property="og:description" content="{{ $desc }}">
|
||||||
|
<meta property="og:url" content="{{ .Permalink }}">
|
||||||
|
<meta property="og:site_name" content="{{ $brand.name }}">
|
||||||
|
<meta property="og:locale" content="{{ replace $brand.defaultLocale "-" "_" }}">
|
||||||
|
<meta property="og:image" content="{{ $ogImage }}">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
|
||||||
|
{{- /* Twitter */ -}}
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="{{ $title }}">
|
||||||
|
<meta name="twitter:description" content="{{ $desc }}">
|
||||||
|
<meta name="twitter:image" content="{{ $ogImage }}">
|
||||||
|
|
||||||
|
{{- /* hreflang alternates for the multilingual build. No-op on single-language
|
||||||
|
sites (.IsTranslated is false), so it is safe to keep permanently. URL
|
||||||
|
scheme: defaultContentLanguageInSubdir = false (English at root). */ -}}
|
||||||
|
{{- if .IsTranslated -}}
|
||||||
|
{{- range .AllTranslations -}}
|
||||||
|
<link rel="alternate" hreflang="{{ .Language.LanguageCode | default .Language.Lang }}" href="{{ .Permalink }}">
|
||||||
|
{{- end -}}
|
||||||
|
{{- /* x-default must point at the site's DEFAULT content language (English at
|
||||||
|
root), independent of the page currently being rendered. site.Languages
|
||||||
|
is ordered by weight, so index 0 is the default language. */ -}}
|
||||||
|
{{- $defaultLang := (index site.Languages 0).Lang -}}
|
||||||
|
{{- range .AllTranslations -}}
|
||||||
|
{{- if eq .Language.Lang $defaultLang -}}
|
||||||
|
<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range .Translations -}}
|
||||||
|
<meta property="og:locale:alternate" content="{{ replace (.Language.LanguageCode | default .Language.Lang) "-" "_" }}">
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ with .OutputFormats.Get "RSS" -}}
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="{{ .RelPermalink }}" title="{{ $brand.name }} RSS">
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ partial "schema-jsonld.html" . }}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{{- /* header.html — sticky site header: logo (brand name from data), primary nav,
|
||||||
|
language switcher and a desktop affiliate CTA.
|
||||||
|
|
||||||
|
The region/compliance data layer (age/RG/currency/payment filtering) is
|
||||||
|
applied headlessly by locale-switcher.html — it no longer renders a visible
|
||||||
|
selector, but its script must still run on every page. */ -}}
|
||||||
|
<header class="sticky top-0 z-40 border-b border-secondary/10 bg-white/95 backdrop-blur">
|
||||||
|
<div class="site-container flex h-16 items-center gap-3 md:gap-4">
|
||||||
|
<a href="{{ "/" | relURL }}" class="flex shrink-0 items-center gap-2 no-underline" aria-label="{{ .Site.Data.brand.name }} home">
|
||||||
|
{{ partial "responsive-image.html" (dict
|
||||||
|
"src" .Site.Data.brand.logoPath
|
||||||
|
"alt" (printf "%s logo" .Site.Data.brand.name)
|
||||||
|
"class" "h-9 w-9"
|
||||||
|
"loading" "eager") }}
|
||||||
|
<span class="text-lg font-extrabold tracking-tight text-secondary">{{ .Site.Data.brand.name }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{ partial "nav.html" . }}
|
||||||
|
|
||||||
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
{{ if gt (len site.Languages) 1 }}{{ partial "language-switcher.html" . }}{{ end }}
|
||||||
|
<span class="hidden sm:inline-flex">
|
||||||
|
{{ partial "cta-button.html" (dict "page" . "text" (i18n "playNow")) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ partial "locale-switcher.html" . }}
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{{- /* language-switcher.html — content LANGUAGE switcher (distinct from the region
|
||||||
|
switcher). The region switcher (locale-switcher.html) changes compliance
|
||||||
|
chrome (age/RG/currency) client-side; THIS switcher navigates to the
|
||||||
|
translated page of the SAME content (linked via front-matter translationKey).
|
||||||
|
Renders nothing on single-language builds. */ -}}
|
||||||
|
{{- if gt (len site.Languages) 1 -}}
|
||||||
|
<div class="relative">
|
||||||
|
<label for="language-switcher" class="sr-only">{{ i18n "selectLanguage" | default "Language" }}</label>
|
||||||
|
<select id="language-switcher" data-language-switcher
|
||||||
|
onchange="if(this.value){window.location.href=this.value;}"
|
||||||
|
class="rounded-md border border-secondary/20 bg-white py-1.5 pl-2 pr-7 text-sm text-secondary focus-visible:outline-primary">
|
||||||
|
{{- $cur := . -}}
|
||||||
|
{{- range site.Languages -}}
|
||||||
|
{{- $lang := . -}}
|
||||||
|
{{- $isCurrent := eq $lang.Lang $cur.Language.Lang -}}
|
||||||
|
{{- $href := "" -}}
|
||||||
|
{{- /* Prefer the direct translation of the current page… */ -}}
|
||||||
|
{{- range $cur.AllTranslations -}}
|
||||||
|
{{- if eq .Language.Lang $lang.Lang -}}{{- $href = .RelPermalink -}}{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- /* …otherwise fall back to that language's home page. */ -}}
|
||||||
|
{{- if not $href -}}
|
||||||
|
{{- with site.Home.AllTranslations -}}
|
||||||
|
{{- range . -}}{{- if eq .Language.Lang $lang.Lang -}}{{- $href = .RelPermalink -}}{{- end -}}{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if not $href -}}{{- $href = "/" -}}{{- end -}}
|
||||||
|
<option value="{{ $href }}"{{ if $isCurrent }} selected{{ end }}>{{ $lang.LanguageName | default (upper $lang.Lang) }}</option>
|
||||||
|
{{- end -}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{{- /* locale-switcher.html — HEADLESS region/compliance data layer.
|
||||||
|
|
||||||
|
This partial no longer renders a visible region selector (the header now
|
||||||
|
exposes only the content language switcher). It still applies the stored
|
||||||
|
or default locale on load so all locale-dependent COMPLIANCE bindings
|
||||||
|
render correctly across the site:
|
||||||
|
[data-age-value] age restriction
|
||||||
|
[data-rg-name] responsible-gambling org name
|
||||||
|
[data-rg-link] responsible-gambling URL
|
||||||
|
[data-locale-currency]currency symbol (payment table)
|
||||||
|
[data-payment-row] show only methods available in the locale
|
||||||
|
[data-kyc-row] highlight the matching KYC row
|
||||||
|
Data comes from data/locales.yaml (exposed as window.__LOCALES__).
|
||||||
|
|
||||||
|
window.applyLocale(loc) and the "localechange" event remain the public
|
||||||
|
API, so a region selector can be reintroduced later without touching the
|
||||||
|
compliance logic. */ -}}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function apply(loc) {
|
||||||
|
var locales = window.__LOCALES__ || [];
|
||||||
|
var entry = null;
|
||||||
|
for (var i = 0; i < locales.length; i++) {
|
||||||
|
if (locales[i].locale === loc) { entry = locales[i]; break; }
|
||||||
|
}
|
||||||
|
if (!entry) {
|
||||||
|
if (!locales.length) return;
|
||||||
|
entry = locales[0];
|
||||||
|
loc = entry.locale;
|
||||||
|
}
|
||||||
|
try { localStorage.setItem("siteLocale", loc); } catch (e) {}
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-age-value]").forEach(function (el) {
|
||||||
|
el.textContent = entry.ageRestriction;
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-rg-name]").forEach(function (el) {
|
||||||
|
el.textContent = entry.responsibleGamblingOrgName;
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-rg-link]").forEach(function (el) {
|
||||||
|
el.setAttribute("href", entry.responsibleGamblingUrl);
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-locale-currency]").forEach(function (el) {
|
||||||
|
el.textContent = entry.currencySymbol;
|
||||||
|
});
|
||||||
|
// Show only payment methods available in the selected locale.
|
||||||
|
document.querySelectorAll("[data-payment-row]").forEach(function (row) {
|
||||||
|
var locs = (row.getAttribute("data-locales") || "").split(",");
|
||||||
|
row.hidden = locs.indexOf(loc) === -1;
|
||||||
|
});
|
||||||
|
// Highlight the KYC row matching the selected locale.
|
||||||
|
document.querySelectorAll("[data-kyc-row]").forEach(function (row) {
|
||||||
|
row.classList.toggle("is-active-locale", row.getAttribute("data-locale") === loc);
|
||||||
|
});
|
||||||
|
// Kept for backwards compatibility if a region selector is reintroduced.
|
||||||
|
document.querySelectorAll("[data-locale-switcher]").forEach(function (sel) {
|
||||||
|
sel.value = loc;
|
||||||
|
});
|
||||||
|
document.dispatchEvent(new CustomEvent("localechange", { detail: { locale: loc, entry: entry } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.applyLocale = apply;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var brand = window.__BRAND__ || {};
|
||||||
|
var stored = null;
|
||||||
|
try { stored = localStorage.getItem("siteLocale"); } catch (e) {}
|
||||||
|
apply(stored || brand.defaultLocale);
|
||||||
|
// If a region selector is present (reintroduced later), wire it up.
|
||||||
|
document.querySelectorAll("[data-locale-switcher]").forEach(function (sel) {
|
||||||
|
sel.addEventListener("change", function () { apply(this.value); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{{- /* nav.html — primary navigation built from pages with showInNav: true,
|
||||||
|
ordered by weight. Desktop = inline list; mobile = Alpine.js toggle. */ -}}
|
||||||
|
{{- $current := . -}}
|
||||||
|
{{- $navPages := sort (where .Site.Pages "Params.showInNav" true) "Weight" -}}
|
||||||
|
|
||||||
|
{{/* Desktop — min-w-0 lets the nav shrink inside the flex row; overflow-x-auto
|
||||||
|
keeps a long section list on one line and scrollable instead of pushing the
|
||||||
|
header past the viewport. */}}
|
||||||
|
<nav aria-label="Primary" class="hidden min-w-0 flex-1 lg:block">
|
||||||
|
<ul class="nav-scroll flex items-center gap-1 overflow-x-auto whitespace-nowrap">
|
||||||
|
{{- range $navPages -}}
|
||||||
|
{{- $active := eq $current.RelPermalink .RelPermalink -}}
|
||||||
|
<li class="shrink-0">
|
||||||
|
<a href="{{ .RelPermalink }}" class="nav-link{{ if $active }} nav-link-active{{ end }}"{{ if $active }} aria-current="page"{{ end }}>{{ .LinkTitle }}</a>
|
||||||
|
</li>
|
||||||
|
{{- end -}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{/* Mobile — native <details> so the menu works with NO JavaScript; Alpine adds
|
||||||
|
escape-to-close and click-outside-to-close when available. */}}
|
||||||
|
<details class="group relative lg:hidden"
|
||||||
|
x-data="{ open: false }"
|
||||||
|
x-bind:open="open"
|
||||||
|
@toggle="open = $event.target.open"
|
||||||
|
@keydown.escape.window="open = false"
|
||||||
|
@click.outside="open = false">
|
||||||
|
<summary class="inline-flex cursor-pointer list-none items-center justify-center rounded-md p-2 text-secondary hover:bg-secondary/5 [&::-webkit-details-marker]:hidden"
|
||||||
|
aria-controls="mobile-menu" aria-label="{{ i18n "toggleMenu" }}">
|
||||||
|
<svg class="h-6 w-6 group-open:hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
<svg class="hidden h-6 w-6 group-open:block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div id="mobile-menu"
|
||||||
|
class="absolute inset-x-0 top-16 z-40 max-h-[calc(100vh-4rem)] overflow-y-auto border-b border-secondary/10 bg-white p-4 shadow-lg">
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
{{- range $navPages -}}
|
||||||
|
<li><a href="{{ .RelPermalink }}" class="nav-link block">{{ .LinkTitle }}</a></li>
|
||||||
|
{{- end -}}
|
||||||
|
<li class="pt-3">{{ partial "cta-button.html" (dict "page" $current "text" (i18n "playNow") "block" true) }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{{- /* payment-table.html — renders data/payment_methods.yaml.
|
||||||
|
Call with (dict "page" . "locale" "<optional locale>").
|
||||||
|
- If "locale" is given, methods are filtered server-side to that market.
|
||||||
|
- Otherwise ALL methods render, and each row carries data-locales so the
|
||||||
|
locale switcher can show/hide rows for the visitor's selected region. */ -}}
|
||||||
|
{{- $page := .page -}}
|
||||||
|
{{- $brand := $page.Site.Data.brand -}}
|
||||||
|
{{- $methods := $page.Site.Data.payment_methods -}}
|
||||||
|
{{- $locale := .locale -}}
|
||||||
|
{{- if $locale -}}
|
||||||
|
{{- $f := slice -}}
|
||||||
|
{{- range $methods -}}{{ if in .availableLocales $locale }}{{ $f = $f | append . }}{{ end }}{{- end -}}
|
||||||
|
{{- $methods = $f -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- $curr := "£" -}}
|
||||||
|
{{- with where $page.Site.Data.locales "locale" $brand.defaultLocale -}}{{- $curr = (index . 0).currencySymbol -}}{{- end -}}
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Method</th>
|
||||||
|
<th scope="col">Type</th>
|
||||||
|
<th scope="col">Min. deposit</th>
|
||||||
|
<th scope="col">Max. deposit</th>
|
||||||
|
<th scope="col">Processing time</th>
|
||||||
|
<th scope="col">Fees</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{- range $methods -}}
|
||||||
|
<tr data-payment-row data-locales="{{ delimit .availableLocales "," }}">
|
||||||
|
<th scope="row" class="font-medium">
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<img src="{{ .logoPath | relURL }}" alt="{{ .name }} logo" width="40" height="24" loading="lazy" class="h-6 w-auto rounded-sm border border-secondary/10">
|
||||||
|
{{ .name }}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<td><span class="badge-muted capitalize">{{ .type }}</span></td>
|
||||||
|
<td><span data-locale-currency>{{ $curr }}</span>{{ .minDeposit }}</td>
|
||||||
|
<td><span data-locale-currency>{{ $curr }}</span>{{ .maxDeposit }}</td>
|
||||||
|
<td>{{ .processingTime }}</td>
|
||||||
|
<td>{{ .fees }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end -}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-secondary/70">Availability and limits vary by region and are shown for your selected market. Select your region in the header to update this table.</p>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{{- /* pros-cons-table.html — reads .Params.pros and .Params.cons (arrays of
|
||||||
|
strings) from the page front matter and renders a two-column comparison. */ -}}
|
||||||
|
{{- $pros := .Params.pros -}}
|
||||||
|
{{- $cons := .Params.cons -}}
|
||||||
|
{{- if or $pros $cons -}}
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="card border-emerald-200 bg-emerald-50">
|
||||||
|
<h3 class="flex items-center gap-2 text-lg font-bold text-emerald-800">
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
Pros
|
||||||
|
</h3>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm text-emerald-900">
|
||||||
|
{{- range $pros }}<li class="flex gap-2"><span aria-hidden="true" class="text-emerald-500">✓</span><span>{{ . }}</span></li>{{ end -}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card border-rose-200 bg-rose-50">
|
||||||
|
<h3 class="flex items-center gap-2 text-lg font-bold text-rose-800">
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
Cons
|
||||||
|
</h3>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm text-rose-900">
|
||||||
|
{{- range $cons }}<li class="flex gap-2"><span aria-hidden="true" class="text-rose-500">✗</span><span>{{ . }}</span></li>{{ end -}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{{- /* rating-widget.html — editorial star rating. Uses page front matter
|
||||||
|
(rating / ratingCount) with a fallback to brand.yaml. */ -}}
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
{{- $rating := .Params.rating | default $brand.ratingValue -}}
|
||||||
|
{{- $count := .Params.ratingCount | default $brand.ratingCount -}}
|
||||||
|
{{- $rounded := int (math.Round $rating) -}}
|
||||||
|
<div class="card flex flex-wrap items-center gap-5">
|
||||||
|
<div class="text-5xl font-extrabold leading-none text-primary">
|
||||||
|
{{ $rating }}<span class="text-xl font-semibold text-secondary/40">/5</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex gap-0.5" role="img" aria-label="Rated {{ $rating }} out of 5">
|
||||||
|
{{- range seq 5 -}}
|
||||||
|
<svg class="h-6 w-6 {{ if le . $rounded }}text-amber-400{{ else }}text-secondary/20{{ end }}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 5.9 20.5l1.4-6.8L2.2 9l6.9-.7L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm text-secondary/60">Independent editorial score based on {{ $count }} data points.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{{- /* related-pages.html — renders the current page's relatedPages front-matter
|
||||||
|
(a list of slugs) as cards. Matches by .Slug or file base name. */ -}}
|
||||||
|
{{- $current := . -}}
|
||||||
|
{{- $related := .Params.relatedPages -}}
|
||||||
|
{{- if $related -}}
|
||||||
|
{{- $found := slice -}}
|
||||||
|
{{- range $slug := $related -}}
|
||||||
|
{{- range $p := $current.Site.Pages -}}
|
||||||
|
{{- if and (ne $p $current) (not $p.IsHome) -}}
|
||||||
|
{{- if or (eq $p.Slug $slug) (and $p.File (eq $p.File.ContentBaseName $slug)) -}}
|
||||||
|
{{- $found = $found | append $p -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- with $found -}}
|
||||||
|
<section class="site-container section" aria-labelledby="related-heading">
|
||||||
|
<h2 id="related-heading" class="text-xl font-bold">Related pages</h2>
|
||||||
|
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{{- range . -}}
|
||||||
|
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline">
|
||||||
|
<span class="badge-primary">{{ .LinkTitle }}</span>
|
||||||
|
<p class="mt-2 text-sm text-secondary/70">{{ .Params.description | default (.Summary | plainify) | truncate 110 }}</p>
|
||||||
|
<span class="mt-3 inline-block text-sm font-semibold text-primary">Read more →</span>
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{{- /* responsible-gambling-block.html — locale-aware RG messaging.
|
||||||
|
Call with (dict "page" . "variant" "compact"|"full").
|
||||||
|
Server renders the DEFAULT locale; the locale switcher updates the
|
||||||
|
[data-age-value] / [data-rg-name] / [data-rg-link] elements client-side. */ -}}
|
||||||
|
{{- $page := .page -}}
|
||||||
|
{{- $variant := .variant | default "full" -}}
|
||||||
|
{{- $brand := $page.Site.Data.brand -}}
|
||||||
|
{{- $locales := $page.Site.Data.locales -}}
|
||||||
|
{{- $default := "" -}}
|
||||||
|
{{- with where $locales "locale" $brand.defaultLocale -}}{{- $default = index . 0 -}}{{- end -}}
|
||||||
|
{{- $age := $brand.ageRestrictionDefault -}}
|
||||||
|
{{- $orgName := "your national support service" -}}
|
||||||
|
{{- $orgUrl := "#" -}}
|
||||||
|
{{- with $default -}}
|
||||||
|
{{- $age = .ageRestriction -}}
|
||||||
|
{{- $orgName = .responsibleGamblingOrgName -}}
|
||||||
|
{{- $orgUrl = .responsibleGamblingUrl -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if eq $variant "compact" -}}
|
||||||
|
<p>This site is intended for adults aged <span data-age-value>{{ $age }}</span>+ only. Gambling should be entertainment, never a way to make money.</p>
|
||||||
|
<p class="mt-2">Need help? Contact <a data-rg-link href="{{ $orgUrl }}" target="_blank" rel="noopener nofollow" class="font-semibold text-white underline"><span data-rg-name>{{ $orgName }}</span></a>.</p>
|
||||||
|
{{- else -}}
|
||||||
|
<div class="prose-content">
|
||||||
|
<p>
|
||||||
|
Gambling must stay fun. This site is intended for adults aged
|
||||||
|
<strong><span data-age-value>{{ $age }}</span>+</strong> only. Never bet more than you can afford to
|
||||||
|
lose, never chase losses, and never gamble to escape stress or debt. If it stops being enjoyable,
|
||||||
|
it is time to stop.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Tools to stay in control</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Deposit limits</strong> — cap how much you can add to your account per day, week or month.</li>
|
||||||
|
<li><strong>Loss & wager limits</strong> — set a ceiling on stakes or net losses over a period.</li>
|
||||||
|
<li><strong>Reality checks</strong> — on-screen reminders of how long you have been playing.</li>
|
||||||
|
<li><strong>Time-outs</strong> — lock yourself out for 24 hours up to several weeks.</li>
|
||||||
|
<li><strong>Self-exclusion</strong> — block access for months or years, including national schemes.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Find local support</h3>
|
||||||
|
<p>
|
||||||
|
Your currently selected region recommends
|
||||||
|
<a data-rg-link href="{{ $orgUrl }}" target="_blank" rel="noopener nofollow"><span data-rg-name>{{ $orgName }}</span></a>.
|
||||||
|
Support services for every market we cover are listed below — all offer free, confidential help.
|
||||||
|
</p>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th scope="col">Region</th><th scope="col">Min. age</th><th scope="col">Support organisation</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{- range $locales -}}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .label | default .locale }}</td>
|
||||||
|
<td>{{ .ageRestriction }}+</td>
|
||||||
|
<td><a href="{{ .responsibleGamblingUrl }}" target="_blank" rel="noopener nofollow">{{ .responsibleGamblingOrgName }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{{- end -}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{{- /* responsive-image.html — safe image renderer.
|
||||||
|
Resize/Fit only work on raster resources (png/jpg/webp/gif). This partial:
|
||||||
|
- resizes + builds srcset for raster Hugo resources,
|
||||||
|
- falls back to a plain <img> for SVG resources or static paths,
|
||||||
|
- falls back to a CSS gradient placeholder when nothing resolves.
|
||||||
|
A missing image can therefore NEVER break the build.
|
||||||
|
|
||||||
|
Call with a dict. Provide ONE of:
|
||||||
|
resource : a Hugo resource (assets/ or page bundle), OR
|
||||||
|
src : a string path (e.g. "/images/logo.svg")
|
||||||
|
Optional: alt, class, loading ("lazy"|"eager"), sizes, widths (slice of ints). */ -}}
|
||||||
|
{{- $r := .resource -}}
|
||||||
|
{{- $src := .src -}}
|
||||||
|
{{- $alt := .alt | default "" -}}
|
||||||
|
{{- $class := .class | default "" -}}
|
||||||
|
{{- $loading := .loading | default "lazy" -}}
|
||||||
|
{{- $sizes := .sizes | default "100vw" -}}
|
||||||
|
{{- $widths := .widths | default (slice 400 800 1200) -}}
|
||||||
|
|
||||||
|
{{- if $r -}}
|
||||||
|
{{- $sub := $r.MediaType.SubType -}}
|
||||||
|
{{- if in (slice "png" "jpg" "jpeg" "webp" "gif") $sub -}}
|
||||||
|
{{- $ow := $r.Width -}}
|
||||||
|
{{- $srcset := slice -}}
|
||||||
|
{{- range $widths -}}
|
||||||
|
{{- if le . $ow -}}
|
||||||
|
{{- $img := $r.Resize (printf "%dx" .) -}}
|
||||||
|
{{- $srcset = $srcset | append (printf "%s %dw" $img.RelPermalink $img.Width) -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $fallback := $r -}}
|
||||||
|
{{- if gt $ow 1200 -}}{{- $fallback = $r.Resize "1200x" -}}{{- end -}}
|
||||||
|
<img
|
||||||
|
src="{{ $fallback.RelPermalink }}"
|
||||||
|
{{ with $srcset }}srcset="{{ delimit . ", " }}" sizes="{{ $sizes }}"{{ end }}
|
||||||
|
width="{{ $fallback.Width }}" height="{{ $fallback.Height }}"
|
||||||
|
alt="{{ $alt }}" loading="{{ $loading }}" decoding="async"
|
||||||
|
{{ with $class }}class="{{ . }}"{{ end }}>
|
||||||
|
{{- else -}}
|
||||||
|
<img src="{{ $r.RelPermalink }}" alt="{{ $alt }}" loading="{{ $loading }}" decoding="async"{{ with $class }} class="{{ . }}"{{ end }}>
|
||||||
|
{{- end -}}
|
||||||
|
{{- else if $src -}}
|
||||||
|
<img src="{{ $src | relURL }}" alt="{{ $alt }}" loading="{{ $loading }}" decoding="async"{{ with $class }} class="{{ . }}"{{ end }}>
|
||||||
|
{{- else -}}
|
||||||
|
<div class="img-placeholder {{ $class }}" role="img" aria-label="{{ $alt }}"></div>
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{{- /* schema-jsonld.html — emits JSON-LD based on the page's front-matter
|
||||||
|
schemaType. BreadcrumbList is emitted separately by breadcrumbs.html.
|
||||||
|
Uses dict + jsonify so all values are safely escaped. */ -}}
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
{{- $type := .Params.schemaType | default "none" -}}
|
||||||
|
|
||||||
|
{{- if eq $type "Organization" -}}
|
||||||
|
{{- $sameAs := slice -}}
|
||||||
|
{{- range $brand.socialLinks -}}
|
||||||
|
{{- $sameAs = $sameAs | append .url -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $data := dict
|
||||||
|
"@context" "https://schema.org"
|
||||||
|
"@type" "Organization"
|
||||||
|
"name" $brand.name
|
||||||
|
"url" .Site.BaseURL
|
||||||
|
"logo" ($brand.logoPath | absURL)
|
||||||
|
"description" $brand.tagline
|
||||||
|
"email" $brand.contactEmail
|
||||||
|
"foundingDate" (printf "%d" (int $brand.foundedYear))
|
||||||
|
"sameAs" $sameAs
|
||||||
|
-}}
|
||||||
|
<script type="application/ld+json">{{ $data | jsonify | safeJS }}</script>
|
||||||
|
|
||||||
|
{{- else if eq $type "FAQPage" -}}
|
||||||
|
{{- $items := slice -}}
|
||||||
|
{{- range .Site.Data.faq -}}
|
||||||
|
{{- $items = $items | append (dict
|
||||||
|
"@type" "Question"
|
||||||
|
"name" .question
|
||||||
|
"acceptedAnswer" (dict "@type" "Answer" "text" .answer)
|
||||||
|
) -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $data := dict
|
||||||
|
"@context" "https://schema.org"
|
||||||
|
"@type" "FAQPage"
|
||||||
|
"mainEntity" $items
|
||||||
|
-}}
|
||||||
|
<script type="application/ld+json">{{ $data | jsonify | safeJS }}</script>
|
||||||
|
|
||||||
|
{{- else if eq $type "Article" -}}
|
||||||
|
{{- /* The About page is an editorial review, so we emit a Review with the
|
||||||
|
promoted operator as the reviewed item. */ -}}
|
||||||
|
{{- $data := dict
|
||||||
|
"@context" "https://schema.org"
|
||||||
|
"@type" "Review"
|
||||||
|
"headline" (.Params.h1 | default .Title)
|
||||||
|
"url" .Permalink
|
||||||
|
"datePublished" (.Date.Format "2006-01-02")
|
||||||
|
"dateModified" (.Lastmod.Format "2006-01-02")
|
||||||
|
"author" (dict "@type" "Organization" "name" $brand.name)
|
||||||
|
"publisher" (dict "@type" "Organization" "name" $brand.name "logo" (dict "@type" "ImageObject" "url" ($brand.logoPath | absURL)))
|
||||||
|
"itemReviewed" (dict "@type" "Organization" "name" $brand.legalOperatorName)
|
||||||
|
"reviewRating" (dict "@type" "Rating" "ratingValue" $brand.ratingValue "bestRating" 5 "worstRating" 1)
|
||||||
|
-}}
|
||||||
|
<script type="application/ld+json">{{ $data | jsonify | safeJS }}</script>
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{{- /* trust-badges.html — compliance/trust strip shown in the footer and reusable
|
||||||
|
elsewhere. Uses the licence authority + age from brand data. */ -}}
|
||||||
|
{{- $brand := .Site.Data.brand -}}
|
||||||
|
<div class="border-t border-white/10 bg-black/20">
|
||||||
|
<div class="site-container flex flex-wrap items-center justify-center gap-4 py-5 text-xs text-white/70">
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1 font-semibold">
|
||||||
|
<span class="grid h-5 w-5 place-items-center rounded-full bg-white/20"><span data-age-value>{{ $brand.ageRestrictionDefault }}</span></span>
|
||||||
|
Over-age play only
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3l7 4v5c0 4.4-3 8.3-7 9-4-0.7-7-4.6-7-9V7l7-4z"/></svg>
|
||||||
|
{{ $brand.licenseAuthority }}
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
SSL secured
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
|
||||||
|
Independent affiliate — not the operator
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: {{ "sitemap.xml" | absURL }}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{{- /* {{< cta text="Play Now" style="primary" size="lg" subid="custom" >}}
|
||||||
|
Reuses partials/cta-button.html so the affiliate link is built in ONE place. */ -}}
|
||||||
|
<div class="my-6">
|
||||||
|
{{ partial "cta-button.html" (dict
|
||||||
|
"page" .Page
|
||||||
|
"text" (.Get "text" | default "Play Now")
|
||||||
|
"style" (.Get "style")
|
||||||
|
"size" (.Get "size")
|
||||||
|
"subid" (.Get "subid")) }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{{- /* Paired shortcode for an inline, no-JS FAQ item:
|
||||||
|
{{< faq-item question="How do I ...?" >}}
|
||||||
|
Markdown answer body.
|
||||||
|
{{< /faq-item >}} */ -}}
|
||||||
|
{{- $q := .Get "question" | default (.Get 0) -}}
|
||||||
|
<details class="my-3 rounded-lg border border-secondary/10 p-4">
|
||||||
|
<summary class="cursor-pointer font-semibold text-secondary">{{ $q }}</summary>
|
||||||
|
<div class="prose-content mt-3">{{ .Inner | markdownify }}</div>
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{- /* {{< payment-table >}} or {{< payment-table locale="de-DE" >}}
|
||||||
|
Renders data/payment_methods.yaml via partials/payment-table.html. */ -}}
|
||||||
|
<div class="my-6">
|
||||||
|
{{ partial "payment-table.html" (dict "page" .Page "locale" (.Get "locale")) }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
{{ partial "breadcrumbs.html" . }}
|
||||||
|
|
||||||
|
<div class="site-container section">
|
||||||
|
<header class="mb-8 max-w-3xl">
|
||||||
|
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">{{ .Params.h1 | default .Title }}</h1>
|
||||||
|
{{ with .Params.description }}<p class="mt-3 text-lg text-secondary/70">{{ . }}</p>{{ end }}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{ with .Content }}<div class="prose-content max-w-3xl">{{ . }}</div>{{ end }}
|
||||||
|
|
||||||
|
{{ $games := sort (where .Site.RegularPages "Section" "slots") "Title" }}
|
||||||
|
<section class="mt-10" aria-label="Game library">
|
||||||
|
{{ if $games }}
|
||||||
|
{{ partial "games-filter.html" (dict "page" . "games" $games) }}
|
||||||
|
{{ else }}
|
||||||
|
<p class="rounded-lg bg-secondary/5 p-6 text-secondary/70">No games have been added yet. Add page bundles under <code>content/slots/games/<slug>/index.md</code>.</p>
|
||||||
|
{{ end }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ if not .Params.hideCta }}
|
||||||
|
<div class="mt-12 max-w-3xl rounded-2xl border border-primary/20 bg-primary/5 p-6">
|
||||||
|
<h2 class="text-xl font-bold">Ready to spin?</h2>
|
||||||
|
<p class="mt-1 text-secondary/70"><span data-age-value>{{ .Site.Data.brand.ageRestrictionDefault }}</span>+ only. T&Cs apply. Please gamble responsibly.</p>
|
||||||
|
<div class="mt-4">{{ partial "cta-button.html" (dict "page" . "text" "Play the Slots" "size" "lg") }}</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ partial "related-pages.html" . }}
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "hugo-igaming-affiliate-template",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Reusable Hugo Extended static-site template for single-brand iGaming (online casino) affiliate websites. One deployment = one brand, driven entirely by config + data + content.",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"gen:images": "node scripts/gen-images.js",
|
||||||
|
"build:css": "tailwindcss -i ./assets/css/tailwind.src.css -o ./dist/tailwind.standalone.css --minify",
|
||||||
|
"build:js": "node -e \"const fs=require('fs'),s='node_modules/alpinejs/dist/cdn.min.js',d='assets/js/vendor/alpine.js';if(fs.existsSync(s)){fs.mkdirSync('assets/js/vendor',{recursive:true});fs.copyFileSync(s,d);console.log('vendor: copied Alpine from node_modules')}else if(fs.existsSync(d)){console.log('vendor: node_modules/alpinejs absent; using committed '+d)}else{console.error('vendor: Alpine missing - run npm install');process.exit(1)}\"",
|
||||||
|
"build": "npm run gen:images && npm run build:css && npm run build:js",
|
||||||
|
"dev": "hugo server -D",
|
||||||
|
"hugo": "hugo",
|
||||||
|
"build:site": "hugo --minify",
|
||||||
|
"clean": "node -e \"const fs=require('fs');['public','resources','dist'].forEach(d=>fs.rmSync(d,{recursive:true,force:true}))\""
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"hugo",
|
||||||
|
"igaming",
|
||||||
|
"casino",
|
||||||
|
"affiliate",
|
||||||
|
"tailwindcss",
|
||||||
|
"static-site"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"hugo-extended": "^0.140.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"postcss-cli": "^11.0.0",
|
||||||
|
"tailwindcss": "^3.4.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* PostCSS configuration used by Hugo Pipes (css.PostCSS) AND the standalone
|
||||||
|
* `npm run build:css` step. Requires the `postcss-cli` devDependency so Hugo
|
||||||
|
* can shell out to the local `postcss` binary during the asset pipeline.
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* ci-build-brand.mjs — assemble ONE brand's Hugo build directory in CI and
|
||||||
|
* (optionally) build it. Portable (Linux runner), no hardcoded paths.
|
||||||
|
*
|
||||||
|
* It merges three sources into a clean build dir:
|
||||||
|
* 1. the TEMPLATE repo (engine): layouts/, assets/, static/, i18n/en.yaml,
|
||||||
|
* package.json(+lock), tailwind.config.js, postcss.config.js, and the
|
||||||
|
* VERIFIED, human-maintained data/locales.yaml + data/payment_methods.yaml
|
||||||
|
* (these are never LLM-generated and must come from the template).
|
||||||
|
* 2. the brand overlay from the content repo: repo/<brand>/{content,data,i18n,hugo.toml}
|
||||||
|
* 3. the template's verified data/ files are copied AFTER the brand overlay so
|
||||||
|
* they always win over anything the pipeline emitted.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node ci-build-brand.mjs \
|
||||||
|
* --template <path-to-template-checkout> \
|
||||||
|
* --brand-dir <path-to repo/<brand>> \
|
||||||
|
* --out <build-dir> \
|
||||||
|
* [--base-url https://<brand>.lilsus.fun/]
|
||||||
|
*
|
||||||
|
* Env fallbacks: TEMPLATE_DIR, BRAND_DIR, BUILD_DIR, BASE_URL.
|
||||||
|
* Exits non-zero on any missing required input so CI fails loudly.
|
||||||
|
*/
|
||||||
|
import { existsSync, rmSync, mkdirSync, cpSync, copyFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { join, basename } from 'node:path';
|
||||||
|
|
||||||
|
function arg(name, envName) {
|
||||||
|
const i = process.argv.indexOf('--' + name);
|
||||||
|
if (i !== -1 && process.argv[i + 1]) return process.argv[i + 1];
|
||||||
|
if (envName && process.env[envName]) return process.env[envName];
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEMPLATE = arg('template', 'TEMPLATE_DIR');
|
||||||
|
const BRAND_DIR = arg('brand-dir', 'BRAND_DIR');
|
||||||
|
const OUT = arg('out', 'BUILD_DIR');
|
||||||
|
const BASE_URL = arg('base-url', 'BASE_URL'); // optional; only echoed as a hint
|
||||||
|
|
||||||
|
function die(msg) { console.error('ci-build-brand: ' + msg); process.exit(1); }
|
||||||
|
if (!TEMPLATE || !existsSync(TEMPLATE)) die('missing/invalid --template (TEMPLATE_DIR): ' + TEMPLATE);
|
||||||
|
if (!BRAND_DIR || !existsSync(BRAND_DIR)) die('missing/invalid --brand-dir (BRAND_DIR): ' + BRAND_DIR);
|
||||||
|
if (!OUT) die('missing --out (BUILD_DIR)');
|
||||||
|
if (!existsSync(join(BRAND_DIR, 'hugo.toml'))) die('brand dir has no hugo.toml: ' + BRAND_DIR);
|
||||||
|
|
||||||
|
const brand = basename(BRAND_DIR);
|
||||||
|
console.log(`ci-build-brand: brand=${brand}`);
|
||||||
|
console.log(` template = ${TEMPLATE}`);
|
||||||
|
console.log(` brandDir = ${BRAND_DIR}`);
|
||||||
|
console.log(` out = ${OUT}`);
|
||||||
|
|
||||||
|
// Fresh build dir.
|
||||||
|
rmSync(OUT, { recursive: true, force: true });
|
||||||
|
mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
// 1. Template engine dirs + files.
|
||||||
|
const TEMPLATE_DIRS = ['layouts', 'assets', 'static', 'i18n', 'scripts'];
|
||||||
|
for (const d of TEMPLATE_DIRS) {
|
||||||
|
const s = join(TEMPLATE, d);
|
||||||
|
if (existsSync(s)) cpSync(s, join(OUT, d), { recursive: true });
|
||||||
|
}
|
||||||
|
const TEMPLATE_FILES = ['package.json', 'package-lock.json', 'tailwind.config.js', 'postcss.config.js'];
|
||||||
|
for (const f of TEMPLATE_FILES) {
|
||||||
|
const s = join(TEMPLATE, f);
|
||||||
|
if (existsSync(s)) copyFileSync(s, join(OUT, f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Brand overlay: content, data, i18n, hugo.toml.
|
||||||
|
for (const d of ['content', 'data', 'i18n']) {
|
||||||
|
const s = join(BRAND_DIR, d);
|
||||||
|
if (existsSync(s)) cpSync(s, join(OUT, d), { recursive: true });
|
||||||
|
}
|
||||||
|
copyFileSync(join(BRAND_DIR, 'hugo.toml'), join(OUT, 'hugo.toml'));
|
||||||
|
|
||||||
|
// Drop the human-note marker if the pipeline emitted it (not valid Hugo data).
|
||||||
|
rmSync(join(OUT, 'data', 'NOTE-locales-payments.md'), { force: true });
|
||||||
|
|
||||||
|
// 3. VERIFIED data that must come from the template and always win.
|
||||||
|
mkdirSync(join(OUT, 'data'), { recursive: true });
|
||||||
|
for (const f of ['locales.yaml', 'payment_methods.yaml']) {
|
||||||
|
const s = join(TEMPLATE, 'data', f);
|
||||||
|
if (existsSync(s)) copyFileSync(s, join(OUT, 'data', f));
|
||||||
|
else console.warn(` WARN: template is missing data/${f} — RG/payment partials may fail`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report.
|
||||||
|
function countMd(dir) {
|
||||||
|
if (!existsSync(dir)) return 0;
|
||||||
|
return readdirSync(dir).filter((f) => f.endsWith('.md')).length;
|
||||||
|
}
|
||||||
|
console.log(` data files: ${existsSync(join(OUT, 'data')) ? readdirSync(join(OUT, 'data')).join(',') : 'none'}`);
|
||||||
|
console.log(` content top-level .md files: ${countMd(join(OUT, 'content'))}`);
|
||||||
|
console.log(`ci-build-brand: build dir ready at ${OUT}`);
|
||||||
|
if (BASE_URL) console.log(` (build with: hugo --source "${OUT}" --gc --minify --baseURL "${BASE_URL}")`);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Site icon">
|
||||||
|
<rect width="32" height="32" rx="7" fill="#0057FF"/>
|
||||||
|
<circle cx="16" cy="16" r="9" fill="none" stroke="#ffffff" stroke-width="2" stroke-dasharray="3 3"/>
|
||||||
|
<circle cx="16" cy="16" r="3.5" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="Brand logo mark">
|
||||||
|
<rect width="40" height="40" rx="9" fill="var(--brand-primary, #0057FF)"/>
|
||||||
|
<circle cx="20" cy="20" r="12" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-dasharray="4 4"/>
|
||||||
|
<circle cx="20" cy="20" r="4.5" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 342 B |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Bitcoin">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<circle cx="28" cy="24" r="12" fill="#f7931a"/>
|
||||||
|
<text x="28" y="30" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" fill="#ffffff">B</text>
|
||||||
|
<text x="54" y="29" font-family="Arial, Helvetica, sans-serif" font-size="12" font-weight="700" text-anchor="middle" fill="#4d4d4d">BTC</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 510 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Mastercard">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<circle cx="34" cy="24" r="11" fill="#eb001b"/>
|
||||||
|
<circle cx="46" cy="24" r="11" fill="#f79e1b" fill-opacity="0.85"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 295 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Neteller">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<text x="40" y="30" font-family="Arial, Helvetica, sans-serif" font-size="13" font-weight="700" text-anchor="middle" fill="#83ba3b">NETELLER</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="PayPal">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<text x="40" y="30" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="700" font-style="italic" text-anchor="middle" fill="#003087">Pay<tspan fill="#009cde">Pal</tspan></text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Paysafecard">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<text x="40" y="21" font-family="Arial, Helvetica, sans-serif" font-size="11" font-weight="700" text-anchor="middle" fill="#00285b">paysafe</text>
|
||||||
|
<text x="40" y="34" font-family="Arial, Helvetica, sans-serif" font-size="11" font-weight="700" text-anchor="middle" fill="#e30613">card</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Skrill">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<text x="40" y="30" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="700" text-anchor="middle" fill="#862165">Skrill</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 319 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Trustly">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<text x="40" y="30" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="700" text-anchor="middle" fill="#0ee06e">Trustly</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" role="img" aria-label="Visa">
|
||||||
|
<rect width="80" height="48" rx="6" fill="#ffffff" stroke="#e5e7eb"/>
|
||||||
|
<text x="40" y="30" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="700" font-style="italic" letter-spacing="0.5" text-anchor="middle" fill="#1a1f71">VISA</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Tailwind CSS 3.x configuration.
|
||||||
|
*
|
||||||
|
* Brand colours are wired to CSS custom properties that Hugo injects into
|
||||||
|
* :root from data/brand.yaml (see assets/css/main.css). This means utility
|
||||||
|
* classes like `bg-primary` / `text-primary` automatically reflect the brand
|
||||||
|
* without any template edits — change data/brand.yaml and rebuild.
|
||||||
|
*/
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./layouts/**/*.html",
|
||||||
|
"./content/**/*.{html,md}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// Mapped to RGB-channel CSS vars injected from data/brand.yaml so that
|
||||||
|
// Tailwind opacity modifiers (e.g. bg-primary/10) work correctly.
|
||||||
|
primary: "rgb(var(--brand-primary-rgb) / <alpha-value>)",
|
||||||
|
secondary: "rgb(var(--brand-secondary-rgb) / <alpha-value>)",
|
||||||
|
"primary-contrast": "rgb(var(--brand-primary-contrast-rgb) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: [
|
||||||
|
"system-ui",
|
||||||
|
"-apple-system",
|
||||||
|
"Segoe UI",
|
||||||
|
"Roboto",
|
||||||
|
"Helvetica",
|
||||||
|
"Arial",
|
||||||
|
"sans-serif",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
prose: "72ch",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||