init: initial commit

- ticker / QR ad overlay / break slide graphics (1920×1080) with all
  geometry derived from one shared module (src/shared/obs-geometry.mjs)
- OBS scene-collection generator (scripts/generate-obs-scenes.mjs) and a
  read-only in-dashboard setup verifier, both driven by the same module
- en/sr dashboard i18n selected via bundle config; configschema.json with
  neutral first-run defaults seeded from optional branding config
- install.sh / systemd user service / update.sh for venue deployment
- reference OBS scene collection + profile from a real venue as fixtures
This commit is contained in:
2026-07-15 17:43:05 +02:00
commit 4bb64a8ca3
71 changed files with 18186 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
/* @import url('https://fonts.googleapis.com/css2?family=Margarine&family=Schoolbell&display=swap'); */
@import "tailwindcss";
@font-face {
font-family: "Margarine";
src: url("./fonts/Margarine-Regular.ttf") format("truetype");
font-weight: 100 900;
font-display: block;
}
/* ── FONT LAB ────────────────────────────────────────────────────────────
Two roles, applied everywhere via the `font-ticker` / `font-display`
utility classes. Swap candidates by editing the two variables below,
save (watch rebuild), refresh the graphic.
- ticker → the scrolling marquee text
- display → big statements: ad row/column text, break slide
Candidates to try (must be installed on the OBS machine, or bundled
below; the stack falls through to the next name if one is missing):
grotesque: "Inter", "Archivo", "Manrope", "Roboto"
condensed: "Oswald", "Archivo Narrow", "Roboto Condensed"
poster: "Anton", "Archivo Black", "Bebas Neue", Impact
To bundle a font file (works offline, recommended for the venue):
drop the .woff2/.ttf into src/graphics/fonts/ and uncomment:
@font-face {
font-family: "MojFont";
src: url("./fonts/MojFont.woff2") format("woff2");
font-weight: 100 900;
font-display: block;
}
Google Fonts also works if the OBS machine has internet — add e.g.
@import url("https://fonts.googleapis.com/css2?family=Anton&display=block");
directly under the tailwindcss import above (keep all @import lines at
the very top of this file). Latin-extended fonts cover č ć đ š ž.
──────────────────────────────────────────────────────────────────────── */
/* ── THEME LAB ───────────────────────────────────────────────────────────
Every brand color the graphics use, in one place. Edit, save (watch
rebuild), refresh the graphic — nothing else references raw colors.
- ticker-* the scrolling marquee: bottom→top gradient, top border,
and the accent used for the recurring blurb text
- ad-bg-* fallback gradient behind ad backgrounds while the image
loads (or when none is uploaded)
- break-bg-* break-slide backdrop gradient (a/b = edges/center)
- break-accent the "where they're from" line on the break slide
- break-blob-* the three drifting ambient blobs shown when no background
image is uploaded (keep them translucent)
──────────────────────────────────────────────────────────────────────── */
@theme {
--font-ticker: "Margarine", system-ui, sans-serif;
--font-display: "Margarine", system-ui, sans-serif;
--color-ticker-start: var(--color-cyan-800);
--color-ticker-end: var(--color-teal-500);
--color-ticker-border: var(--color-blue-400);
--color-ticker-accent: var(--color-amber-200);
--color-ad-bg-start: #0b1020;
--color-ad-bg-end: #1a2140;
--color-break-bg-a: #0a0e1a;
--color-break-bg-b: #171030;
--color-break-accent: color-mix(in srgb, var(--color-amber-300) 90%, transparent);
--color-break-blob-1: rgba(99, 102, 241, 0.35);
--color-break-blob-2: rgba(168, 85, 247, 0.3);
--color-break-blob-3: rgba(245, 158, 11, 0.18);
}
html,
body {
margin: 0;
background: transparent;
overflow: hidden;
}
/* Break slide ambient motion */
@keyframes drift-a {
from {
transform: translate(-120px, -80px) scale(1);
}
to {
transform: translate(180px, 140px) scale(1.2);
}
}
@keyframes drift-b {
from {
transform: translate(120px, 60px) scale(1.1);
}
to {
transform: translate(-160px, -110px) scale(0.9);
}
}
@keyframes drift-c {
from {
transform: translate(0, 120px) scale(1);
}
to {
transform: translate(100px, -140px) scale(1.3);
}
}
/* Ad background: slow pan; the 1.15 scale gives the ±3% translate room to
move without ever exposing an edge (7.5% margin > 3% × 1.15 travel) */
@keyframes ad-pan {
from {
transform: scale(1.15) translate(-3%, -2%);
}
to {
transform: scale(1.15) translate(3%, 2%);
}
}
/* Break background: full edge-to-edge sweep that visibly reverses. At 1.15
scale the hidden margin is 7.5% of the screen per side, i.e. ~6.5% of the
element's own size — ±6%/±4% stays just inside that, so each bounce lands
right at the image edge without exposing it */
@keyframes break-bounce {
from {
transform: scale(1.15) translate(-6%, -4%);
}
to {
transform: scale(1.15) translate(6%, 4%);
}
}
@keyframes breathe {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.045);
}
}