feat: přidání K-ON! theme
CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 27s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m52s
CI / Build and push Docker image (push) Successful in 46s
CI / Notify (push) Successful in 2s

This commit is contained in:
mates committed 2026-10-13 10:49:14 +02:00
1 parent e570f308cd
commit 4bc2151cc4
10 files changed
+873 -12

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ import './App.scss';
import './themes/umamusume.scss';
import './themes/mlp.scss';
import './themes/hellgirl.scss';
import './themes/kon.scss';
import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons';
import { useSettings } from './context/settings';
import Footer from './components/Footer';
+11
View File
@@ -40,6 +40,17 @@
box-shadow: 0 0 0 2px #c8102e, 0 0 6px rgb(200 16 46 / 0.5);
}
// Styl K-ON!: emoji na krémovém „trsátku" s lemem v barvě kytary Gitah (cherry sunburst)
.eater-badge-kon {
display: inline-grid;
place-items: center;
font-size: 16px;
line-height: 1;
border-radius: 50% 50% 50% 50% / 42% 42% 58% 58%;
background: radial-gradient(circle at 40% 35%, #fffaf0, #fbe6c8);
box-shadow: 0 0 0 2px #d43f62, 0 2px 0 3px #27315f;
}
// Odznak a jméno drží na jednom řádku (úzký sloupec „Člen" na stránce objednávek)
.user-info:has(.eater-badge) {
display: inline-flex;
+18 -4
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { useSettings } from "../context/settings";
import { StyleTheme, useSettings } from "../context/settings";
import { UmaCharacter, loadUmaCharacters } from "../utils/umaCharacters";
import "./EaterBadge.scss";
@@ -25,12 +25,25 @@ const HG_LEVELS = [
{ minItems: 2, emoji: '🥀', title: 'Úroveň Ai Enma — tichý, ale nenasytný apetit' },
];
/** Úrovně ve stylu K-ON! — emoji v odznaku jako trsátko. Seřazeno od nejvyšší úrovně. */
const KON_LEVELS = [
{ minItems: 3, emoji: '🍰', title: 'Úroveň Yui — sní všechno a nepřibere ani gram' },
{ minItems: 2, emoji: '☕', title: 'Úroveň Tea Time — k obědu ještě čaj a dortík' },
];
/** Úrovně podle stylu pro odznaky s emoji. */
const EMOJI_LEVELS: Partial<Record<StyleTheme, typeof MLP_LEVELS>> = {
mlp: MLP_LEVELS,
hellgirl: HG_LEVELS,
kon: KON_LEVELS,
};
type Props = {
/** Počet jídel/položek, které si strávník vybral či objednal */
items: number,
};
/** Odznak s postavou u strávníků s více jídly — jen ve stylech Umamusume, My Little Pony a Hell Girl. */
/** Odznak s postavou u strávníků s více jídly — jen ve stylech Umamusume, My Little Pony, Hell Girl a K-ON!. */
export default function EaterBadge({ items }: Readonly<Props>) {
const styleTheme = useSettings()?.styleTheme;
const isUma = styleTheme === 'umamusume';
@@ -40,8 +53,9 @@ export default function EaterBadge({ items }: Readonly<Props>) {
if (isUma) loadUmaCharacters().then(setCharacters);
}, [isUma]);
if (styleTheme === 'mlp' || styleTheme === 'hellgirl') {
const emojiLevel = (styleTheme === 'mlp' ? MLP_LEVELS : HG_LEVELS).find(l => items >= l.minItems);
const emojiLevels = styleTheme && EMOJI_LEVELS[styleTheme];
if (emojiLevels) {
const emojiLevel = emojiLevels.find(l => items >= l.minItems);
if (!emojiLevel) return null;
return (
<OverlayTrigger placement="top" overlay={<Tooltip>{emojiLevel.title} ({items} jídla)</Tooltip>}>
+74
View File
@@ -326,3 +326,77 @@
font-family: 'Shippori Mincho', 'Noto Serif JP', serif;
}
}
// ============================================
// Konfety ve stylu K-ON!
// ============================================
.confetti-kon {
filter: drop-shadow(0 3px 3px rgb(39 49 95 / 0.25));
}
// Samolepky jako na pouzdru kytary — bílý okraj a lehce natočené
.confetti-kon-badge {
padding: 4px 14px;
border-radius: 10px;
border: 3px solid #fff;
color: #fff;
font-family: 'Zen Maru Gothic', 'M PLUS Rounded 1c', sans-serif;
font-weight: 900;
font-size: 1.05rem;
white-space: nowrap;
text-shadow: 0 1px 0 rgb(0 0 0 / 0.2);
box-shadow: 0 3px 0 rgb(39 49 95 / 0.25);
}
// Nápis „Houkago Tea Time" — vstupenka na školní koncert s notovou osnovou
.confetti-kon-banner {
position: absolute;
top: 38%;
left: 50%;
transform: translate(-50%, -50%);
padding: 14px 44px;
font-family: 'Zen Maru Gothic', 'M PLUS Rounded 1c', sans-serif;
font-weight: 900;
font-size: clamp(1.6rem, 6vw, 3.4rem);
white-space: nowrap;
color: #27315f;
border-radius: 18px;
background:
repeating-linear-gradient(180deg, transparent 0 9px, rgb(39 49 95 / 0.12) 9px 10px) center / 100% 50px no-repeat,
#fffaf0;
border: 4px solid #27315f;
box-shadow: 0 0 0 6px #fffaf0, 0 0 0 9px #d43f62, 0 12px 26px rgb(0 0 0 / 0.25);
animation: kon-banner 2.2s cubic-bezier(0.3, 1.5, 0.5, 1) both;
}
// Vyskočí do rytmu (raz-dva) a odplave nahoru
@keyframes kon-banner {
0% { transform: translate(-50%, -50%) scale(0.3) rotate(6deg); opacity: 0; }
18% { transform: translate(-50%, -50%) scale(1.1) rotate(-3deg); opacity: 1; }
28% { transform: translate(-50%, -50%) scale(1) rotate(-2deg); }
40% { transform: translate(-50%, -50%) scale(1.05) rotate(-2deg); }
50% { transform: translate(-50%, -50%) scale(1) rotate(-2deg); }
80% { transform: translate(-50%, -50%) scale(1) rotate(-2deg); opacity: 1; }
100% { transform: translate(-50%, -150%) scale(0.9) rotate(-2deg); opacity: 0; }
}
// Postava s hláškou — krémový odznak a bublina jako stránka notového sešitu
.confetti-kon-voice {
.confetti-mlp-avatar {
background: radial-gradient(circle at 40% 35%, #fffaf0, #fbe6c8);
box-shadow: 0 0 0 4px var(--mlp-speaker, #d43f62), 0 4px 10px rgb(0 0 0 / 0.3);
}
.confetti-mlp-bubble {
background:
repeating-linear-gradient(180deg, transparent 0 21px, rgb(39 49 95 / 0.08) 21px 22px),
#fffaf0;
color: #27315f;
border-radius: 16px;
font-family: 'Zen Maru Gothic', 'M PLUS Rounded 1c', sans-serif;
p {
font-weight: 700;
}
}
}
+24 -4
View File
@@ -48,6 +48,21 @@ const HG_SPEAKERS = [
{ name: 'Kikuri', emoji: '🎀', color: '#d4407a', quote: 'Hihi! Tohle bych do pekla poslala jako první!' },
{ name: 'Babička', emoji: '🕸️', color: '#8c7672', quote: 'Ai, oběd je na stole… jako každý den už čtyři sta let.' },
];
const KON_EMOJI = ['🎸', '🥁', '🎹', '🎵', '🎶', '🍰', '☕', '🍓', '🐢', '🎀', '🍪', '🫖'];
// Hlášky z K-ON! (Houkago Tea Time, Fuwa Fuwa Time, Gitah, Ton-chan, Mugi a její čaj...)
const KON_BADGES = ['放課後ティータイム', 'Fuwa Fuwa Time ♪', 'Tea time!', 'Gitah!', 'Ton-chan ♡', 'けいおん!', 'Nejdřív čaj, pak zkouška', "Don't say lazy", 'Azunyan~', 'Ui!'];
const KON_BADGE_COLORS = ['#d43f62', '#3d6fc4', '#e8a10f', '#8a6bc7', '#e46f9a', '#27315f'];
/** Členky Houkago Tea Time (+ Ui, Sawa-chan a Ton-chan) s „hláškou" po finálním výsledku. */
const KON_SPEAKERS = [
{ name: 'Yui Hirasawa', emoji: '🎸', color: '#d43f62', quote: 'Gitah říká, že tohle bude mňamka! Fuwa fuwa~' },
{ name: 'Mio Akiyama', emoji: '🎼', color: '#3d6fc4', quote: 'Dobře… ale hned po obědě se jde zkoušet! Opravdu!' },
{ name: 'Ritsu Tainaka', emoji: '🥁', color: '#e8a10f', quote: 'Prezidentka klubu rozhodla! Raz, dva, raz dva tři čtyři!' },
{ name: 'Tsumugi Kotobuki', emoji: '🫖', color: '#c9a227', quote: 'Čaj a dortík k tomu přinesu já. Těším se!' },
{ name: 'Azusa Nakano', emoji: '🐱', color: '#8a6bc7', quote: 'Zase jídlo místo zkoušky? …No dobře, ale jen chvilku!' },
{ name: 'Ui Hirasawa', emoji: '🍳', color: '#e46f9a', quote: 'Sestřičko, nezapomeň si vzít ubrousek!' },
{ name: 'Sawako Yamanaka', emoji: '👗', color: '#27315f', quote: 'Oběd? Výborně. Mezitím vám ušiju nové kostýmy!' },
{ name: 'Ton-chan', emoji: '🐢', color: '#3c9a6b', quote: '(spokojeně mává ploutvičkou)' },
];
const DURATION_MS = 6400;
const rand = (min: number, max: number) => min + Math.random() * (max - min);
@@ -134,10 +149,12 @@ function buildPieces(theme: WheelTheme, big: boolean, images: UmaImage[]): Piece
}));
}
const count = big ? 50 : 24;
if (theme === 'mlp' || theme === 'hellgirl') {
if (theme === 'mlp' || theme === 'hellgirl' || theme === 'kon') {
const [prefix, badges, colors, emoji] = theme === 'mlp'
? ['confetti-mlp', MLP_BADGES, MLP_BADGE_COLORS, MLP_EMOJI]
: ['confetti-hg', HG_BADGES, HG_BADGE_COLORS, HG_EMOJI];
: theme === 'hellgirl'
? ['confetti-hg', HG_BADGES, HG_BADGE_COLORS, HG_EMOJI]
: ['confetti-kon', KON_BADGES, KON_BADGE_COLORS, KON_EMOJI];
return Array.from({ length: count }, (_, id) => {
const base: React.CSSProperties = {
...launchStyle(pick(LAUNCHES)),
@@ -201,9 +218,11 @@ export default function WheelConfetti({ theme, burstKey, big = false }: Readonly
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [burstKey]);
// MLP: hlášku pronese náhodná postava z Mane 6, Hell Girl: Ai Enma či některý z jejích služebníků
// MLP: hlášku pronese náhodná postava z Mane 6, Hell Girl: Ai Enma či některý z jejích služebníků,
// K-ON!: některá z členek Houkago Tea Time
const emojiSpeaker = useMemo(
() => big ? (theme === 'mlp' ? pick(MLP_SPEAKERS) : theme === 'hellgirl' ? pick(HG_SPEAKERS) : undefined) : undefined,
() => big ? (theme === 'mlp' ? pick(MLP_SPEAKERS) : theme === 'hellgirl' ? pick(HG_SPEAKERS)
: theme === 'kon' ? pick(KON_SPEAKERS) : undefined) : undefined,
// eslint-disable-next-line react-hooks/exhaustive-deps
[burstKey],
);
@@ -215,6 +234,7 @@ export default function WheelConfetti({ theme, burstKey, big = false }: Readonly
{theme === 'umamusume' && big && <div className="confetti-uma-banner">🏆 1着 🏆</div>}
{theme === 'mlp' && big && <div className="confetti-mlp-banner">🌈 Sonic Rainboom! 🌈</div>}
{theme === 'hellgirl' && big && <div className="confetti-hg-banner">🏮 地獄流し 🏮</div>}
{theme === 'kon' && big && <div className="confetti-kon-banner">🎸 放課後ティータイム 🍰</div>}
{pieces.map(p => <span key={p.id} className={p.className} style={p.style}>{p.content}</span>)}
{speaker && (
<div className="confetti-uma-voice">
@@ -1013,6 +1013,261 @@
}
}
// ============================================
// STYL K-ON! kola štěstí (dle stylu zvoleného v nastavení).
// Výseče v barvách členek Houkago Tea Time, tmavě modré školní sako,
// notový sešit a tlačítko roztočení jako kytara Gitah (cherry sunburst).
// ============================================
.spin-wheel-modal.spin-theme-kon {
--kon-font: 'Zen Maru Gothic', 'M PLUS Rounded 1c', 'Nunito', sans-serif;
--kon-blazer: #27315f;
--kon-blazer-light: #3a4782;
--kon-blazer-shadow: #171d3d;
--kon-cherry: #c43358;
--kon-cherry-deep: #a3234a;
--kon-sunburst: #f2a33a;
--kon-panel: #fffaf0;
--kon-border: #e6d8c0;
--kon-text: #222849;
// Yui, Mio, Ritsu, Azusa, Ui a tmavě modrá školní
--wheel-c0: #d43f62;
--wheel-c1: #3d6fc4;
--wheel-c2: #f2b81d;
--wheel-c3: #8a6bc7;
--wheel-c4: #e46f9a;
--wheel-c5: #27315f;
--wheel-rim: #27315f;
--wheel-hub: #fffaf0;
font-family: var(--kon-font);
.modal-content {
border: 3px solid var(--kon-border);
border-radius: 22px;
overflow: hidden;
background:
repeating-linear-gradient(180deg, transparent 0 27px, rgb(61 111 196 / 0.07) 27px 28px),
var(--kon-panel);
color: var(--kon-text);
}
.modal-header {
background:
linear-gradient(90deg, #d43f62 0 20%, #3d6fc4 20% 40%, #f2b81d 40% 60%, #e8cf7a 60% 80%, #8a6bc7 80% 100%) bottom / 100% 4px no-repeat,
linear-gradient(90deg, var(--kon-blazer-shadow), var(--kon-blazer) 40%, var(--kon-blazer-light));
border-bottom: none;
h2 {
color: #fff;
font-family: var(--kon-font);
font-weight: 900;
text-shadow: 0 2px 0 var(--kon-blazer-shadow);
}
.btn-close {
filter: invert(1) grayscale(1) brightness(2);
opacity: 0.9;
}
}
.modal-footer {
border-top: 2px dashed var(--kon-border);
}
.spin-steps span {
font-family: var(--kon-font);
background: #f3e8d4;
color: #8a8aa3;
border-radius: 10px;
padding: 4px 16px;
&.active {
background: var(--kon-blazer);
color: #fff;
}
&.done {
background: var(--kon-cherry);
color: #fff;
}
}
.btn {
font-family: var(--kon-font);
font-weight: 700;
border-radius: 10px;
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.2s ease;
&:active:not(:disabled) {
transform: translateY(3px);
box-shadow: 0 1px 0 var(--kon-btn-shadow, transparent);
}
}
.btn-primary {
--kon-btn-shadow: var(--kon-blazer);
background: linear-gradient(180deg, #e0567a, var(--kon-cherry-deep));
border: none;
box-shadow: 0 4px 0 var(--kon-blazer);
color: #fff;
text-shadow: 0 1px 0 rgb(0 0 0 / 0.2);
&:disabled {
background: #c8c3cf;
box-shadow: 0 4px 0 #a5a1b0;
}
}
// Tlačítko roztočení — lak kytary Gitah (jantarový střed, třešňové okraje)
.spin-button {
--kon-btn-shadow: var(--kon-blazer);
background: radial-gradient(ellipse 70% 130% at 50% 45%, #ffd27a, var(--kon-sunburst) 25%, var(--kon-cherry) 65%, #6e1530);
box-shadow: 0 5px 0 var(--kon-blazer), 0 0 18px rgb(242 163 58 / 0.45);
font-size: 1.15rem;
padding: 12px 36px;
}
.btn-outline-primary {
--kon-btn-shadow: var(--kon-border);
background: #fff;
border: 2px solid var(--kon-blazer);
color: var(--kon-blazer);
box-shadow: 0 4px 0 var(--kon-border);
&:hover {
background: var(--kon-blazer);
color: #fff;
}
}
.btn-secondary {
--kon-btn-shadow: var(--kon-border);
background: #fff;
border: 2px solid var(--kon-border);
color: var(--kon-text);
box-shadow: 0 4px 0 var(--kon-border);
}
.spin-source {
background: #f3e8d4;
> .btn-primary {
background: linear-gradient(180deg, #e0567a, var(--kon-cherry-deep));
box-shadow: 0 2px 0 var(--kon-blazer) !important;
}
> .btn-outline-primary {
background: transparent;
color: var(--kon-blazer);
&:hover {
background: #fff;
}
}
}
.spin-result {
font-weight: 700;
strong {
color: var(--kon-cherry);
}
}
.spin-options {
background: var(--kon-panel);
border: 2px solid var(--kon-border);
border-radius: 16px;
li:hover {
background: #fdeee0;
}
}
.spin-options-header {
border-bottom: 2px solid var(--kon-border);
color: var(--kon-blazer);
}
.spin-empty {
border-color: var(--kon-border);
}
// Ukazatel jako trsátko
.spin-wheel-pointer {
border-top-color: var(--kon-cherry);
filter: drop-shadow(0 2px 0 var(--kon-blazer));
}
.spin-seg {
stroke: #fffaf0;
stroke-width: 2.5;
}
.spin-seg-label {
font-family: var(--kon-font);
font-weight: 700;
}
}
// Tmavý režim — večerní koncert pod reflektory
[data-bs-theme="dark"] .spin-wheel-modal.spin-theme-kon {
--kon-cherry: #ff7a9a;
--kon-panel: #171c42;
--kon-border: #2c3566;
--kon-text: #f1f0ff;
--wheel-c5: #4a5ab0;
--wheel-rim: #8fb2ff;
--wheel-hub: #171c42;
.modal-content {
background:
radial-gradient(ellipse 45% 60% at 10% -10%, rgb(255 122 154 / 0.18), transparent 70%),
radial-gradient(ellipse 45% 60% at 90% -10%, rgb(255 209 102 / 0.14), transparent 70%),
#0e1230;
}
.spin-steps span:not(.active):not(.done) {
background: #212858;
color: #8085b3;
}
.btn-outline-primary, .btn-secondary {
background: #171c42;
color: #f1f0ff;
}
.btn-outline-primary {
border-color: #8fb2ff;
&:hover {
background: #2b367a;
}
}
.spin-source {
background: #212858;
> .btn-outline-primary {
background: transparent;
color: #bfc2e6;
&:hover {
background: #171c42;
color: #fff;
}
}
}
.spin-options-header {
color: #c9d6ff;
}
.spin-options li:hover {
background: #232a5c;
}
}
// Ikona kola se při najetí pootočí
.spin-wheel-icon {
transition: transform 0.4s ease !important;
+3 -2
View File
@@ -12,13 +12,14 @@ const SHOW_BUTTERFLIES_KEY = 'show_butterflies';
export type ThemePreference = 'system' | 'light' | 'dark';
/** Vizuální styl aplikace (nezávislý na světlém/tmavém režimu). */
export type StyleTheme = 'simple' | 'umamusume' | 'mlp' | 'hellgirl';
export type StyleTheme = 'simple' | 'umamusume' | 'mlp' | 'hellgirl' | 'kon';
export const STYLE_THEMES: { value: StyleTheme, label: string }[] = [
{ value: 'simple', label: 'Jednoduchý' },
{ value: 'umamusume', label: 'Umamusume' },
{ value: 'mlp', label: 'My Little Pony' },
{ value: 'hellgirl', label: 'Hell Girl' },
{ value: 'kon', label: 'K-ON!' },
];
export type SettingsContextProps = {
@@ -234,7 +235,7 @@ function useProvideSettings(): SettingsContextProps {
}, [showButterflies]);
// Aplikuje accent barvy při změně hue nebo přepnutí světlý/tmavý.
// Tematické styly (Umamusume, My Little Pony, Hell Girl) mají vlastní pevnou paletu, accent barvu proto uplatňuje jen jednoduchý styl.
// Tematické styly (Umamusume, My Little Pony, Hell Girl, K-ON!) mají vlastní pevnou paletu, accent barvu proto uplatňuje jen jednoduchý styl.
useEffect(() => {
localStorage.setItem(ACCENT_HUE_KEY, String(accentHue));
if (styleTheme === 'simple') {
+2
View File
@@ -11,6 +11,8 @@ const ICONS: Record<StyleTheme, Record<GameId, string>> = {
mlp: { reaction: '🌈', snake: '🦄', tetris: '💎', pong: '🎈', drawing: '🖌️' },
// Pekelná korespondence: plamen, rudá nit, lampion, slaměná panenka, svitek
hellgirl: { reaction: '🔥', snake: '🪢', tetris: '🏮', pong: '🪆', drawing: '📜' },
// Klub lehké hudby: bicí Ritsu, želva Ton-chan, dortíky k čaji, kytara Gitah a notový zápis
kon: { reaction: '🥁', snake: '🐢', tetris: '🍰', pong: '🎸', drawing: '🎼' },
};
/** Styl z <html data-luncher-theme> — pro místa mimo React kontext nastavení. */
+483
View File
@@ -0,0 +1,483 @@
// ============================================
// STYL "K-ON!" (volitelný, výchozí je jednoduchý styl)
// Inspirováno anime K-ON! (けいおん!) — klub lehké hudby na dívčí škole Sakuragaoka.
// Tmavě modré školní sako, kytara Gitah v barvě cherry sunburst, mléčný čaj
// a dortíky z odpoledních dýchánků (Houkago Tea Time), notová osnova
// a ručně psané nápisy jako ze školního sešitu.
// Tmavý režim = večerní koncert v klubu (live house) pod reflektory.
// Aktivní, pokud má <html> atribut data-luncher-theme="kon".
// ============================================
$kon: 'html[data-luncher-theme="kon"]';
// Pruh v barvách členek Houkago Tea Time: Yui (červená Gitah), Mio (modrá),
// Ritsu (žlutá), Mugi (zlatavá) a Azusa (fialová)
$kon-members: linear-gradient(90deg, #d43f62 0 20%, #3d6fc4 20% 40%, #f2b81d 40% 60%, #e8cf7a 60% 80%, #8a6bc7 80% 100%);
// Notová osnova (pět linek) — opakuje se na pozadí po 220 px
$kon-staff-stops: transparent 0 100px,
var(--kon-staff) 100px 101px, transparent 101px 109px,
var(--kon-staff) 109px 110px, transparent 110px 118px,
var(--kon-staff) 118px 119px, transparent 119px 127px,
var(--kon-staff) 127px 128px, transparent 128px 136px,
var(--kon-staff) 136px 137px, transparent 137px;
// Rozeseté noty ♪ ♫ (ve dne červená a modrá, v noci světlejší)
$kon-notes: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Ctext x='34' y='95' font-size='26' fill='%23d43f62' fill-opacity='0.16'%3E%E2%99%AA%3C/text%3E%3Ctext x='150' y='128' font-size='30' fill='%233d6fc4' fill-opacity='0.14'%3E%E2%99%AB%3C/text%3E%3Ctext x='96' y='210' font-size='18' fill='%23e8a10f' fill-opacity='0.18'%3E%E2%99%AA%3C/text%3E%3C/svg%3E");
$kon-notes-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Ctext x='34' y='95' font-size='26' fill='%23ff8fa8' fill-opacity='0.22'%3E%E2%99%AA%3C/text%3E%3Ctext x='150' y='128' font-size='30' fill='%238fb2ff' fill-opacity='0.2'%3E%E2%99%AB%3C/text%3E%3Ctext x='96' y='210' font-size='18' fill='%23ffd166' fill-opacity='0.22'%3E%E2%99%AA%3C/text%3E%3C/svg%3E");
#{$kon} {
--kon-font: 'Zen Maru Gothic', 'M PLUS Rounded 1c', 'Nunito', -apple-system, sans-serif;
--kon-hand: 'Yusei Magic', 'Zen Maru Gothic', 'M PLUS Rounded 1c', sans-serif;
--kon-blazer: #27315f; // tmavě modré sako Sakuragaoka
--kon-blazer-light: #3a4782;
--kon-blazer-shadow: #171d3d;
--kon-cherry: #c43358; // Gitah — cherry sunburst
--kon-cherry-deep: #a3234a;
--kon-sunburst: #f2a33a; // jantarový střed sunburstu
--kon-ritsu: #f2b81d;
--kon-tea: #fbf3e4; // mléčný čaj
--kon-paper: #fffaf0; // notový sešit
--kon-card-border: #e6d8c0;
--kon-staff: rgb(39 49 95 / 0.07);
--luncher-primary: var(--kon-cherry);
--luncher-primary-hover: var(--kon-cherry-deep);
--luncher-primary-light: #fbe4ea;
--luncher-bg: var(--kon-tea);
--luncher-bg-card: var(--kon-paper);
--luncher-bg-hover: #f8ecd8;
--luncher-text: #222849;
--luncher-text-secondary: #4a5179;
--luncher-text-muted: #8a8aa3;
--luncher-border: var(--kon-card-border);
--luncher-border-light: #f0e6d3;
--luncher-navbar-bg: var(--kon-blazer);
--luncher-navbar-text: #ffffff;
--luncher-action-icon: var(--kon-cherry);
--luncher-buyer-icon: #e8a10f;
--luncher-success: #2f8a57;
--luncher-warning: #d98a0b;
--luncher-danger: #d0304a;
--luncher-radius-sm: 10px;
--luncher-radius: 14px;
--luncher-radius-lg: 18px;
--luncher-radius-xl: 22px;
--luncher-shadow: 0 4px 14px -6px rgb(39 49 95 / 0.25);
--luncher-shadow-lg: 0 10px 26px -8px rgb(39 49 95 / 0.35);
&[data-bs-theme="dark"] {
// Večerní koncert v klubu — reflektory nad pódiem
--kon-blazer: #1a2150;
--kon-blazer-light: #2b367a;
--kon-blazer-shadow: #0a0e26;
--kon-cherry: #ff7a9a;
--kon-cherry-deep: #e85a7e;
--kon-ritsu: #ffd166;
--kon-card-border: #2c3566;
--kon-staff: rgb(180 196 255 / 0.06);
--luncher-primary: #ff86a3;
--luncher-primary-hover: #ffa8bd;
--luncher-primary-light: #3a1d35;
--luncher-bg: #0e1230;
--luncher-bg-card: #171c42;
--luncher-bg-hover: #212858;
--luncher-text: #f1f0ff;
--luncher-text-secondary: #bfc2e6;
--luncher-text-muted: #8085b3;
--luncher-border: #2c3566;
--luncher-border-light: #232a58;
--luncher-navbar-bg: #141a44;
--luncher-action-icon: #ff86a3;
--luncher-buyer-icon: #ffd166;
--luncher-success: #6fd59a;
--luncher-warning: #ffd166;
--luncher-danger: #ff6f86;
--luncher-shadow: 0 4px 16px -4px rgb(0 0 0 / 0.55);
--luncher-shadow-lg: 0 12px 30px -8px rgb(0 0 0 / 0.65);
}
body {
font-family: var(--kon-font);
background-color: var(--luncher-bg);
// Notový sešit: osnova s notami, nahoře teplé odpolední světlo klubovny
background-image:
#{$kon-notes},
linear-gradient(180deg, #{$kon-staff-stops}),
linear-gradient(180deg, #fde4d6 0, var(--luncher-bg) 360px);
background-size: 220px 220px, 100% 220px, 100% 100%;
background-attachment: fixed;
}
&[data-bs-theme="dark"] body {
// Dva reflektory (růžový a jantarový) svítí z horních rohů na pódium
background-image:
#{$kon-notes-dark},
linear-gradient(180deg, #{$kon-staff-stops}),
radial-gradient(ellipse 45% 60% at 10% -10%, rgb(255 122 154 / 0.18), transparent 70%),
radial-gradient(ellipse 45% 60% at 90% -10%, rgb(255 209 102 / 0.14), transparent 70%),
linear-gradient(180deg, #1a1f4d 0, var(--luncher-bg) 420px);
background-size: 220px 220px, 100% 220px, 100% 100%, 100% 100%, 100% 100%;
}
.app-container {
background: transparent;
}
h1, h2, h3, h4, .navbar-brand, .btn {
font-family: var(--kon-font);
}
// ---------- Navbar: školní sako s pruhem v barvách kapely ----------
.navbar {
background:
#{$kon-members} bottom / 100% 4px no-repeat,
linear-gradient(90deg, var(--kon-blazer-shadow), var(--kon-blazer) 40%, var(--kon-blazer-light)) !important;
padding-bottom: calc(var(--bs-navbar-padding-y, 0.5rem) + 4px);
box-shadow: 0 3px 10px -2px rgb(23 29 61 / 0.4);
.navbar-brand {
font-weight: 900;
letter-spacing: 0.5px;
text-shadow: 0 2px 0 var(--kon-blazer-shadow);
// Nota, která poskakuje do rytmu
&::after {
content: '♪';
display: inline-block;
margin-left: 6px;
color: var(--kon-ritsu);
font-size: 0.85em;
vertical-align: super;
animation: kon-bounce 1.2s ease-in-out infinite;
}
}
.dropdown-menu {
border: 2px solid var(--kon-card-border);
border-radius: var(--luncher-radius);
}
}
// ---------- Titulek dne: ručně psaná cedulka z dveří klubovny ----------
.day-navigator .title {
position: relative;
display: inline-block;
padding: 4px 26px 6px;
font-family: var(--kon-hand);
font-weight: 400;
font-size: 1.8rem;
color: var(--kon-blazer) !important;
background:
repeating-linear-gradient(180deg, transparent 0 11px, rgb(61 111 196 / 0.12) 11px 12px),
var(--kon-paper);
border: 3px solid var(--kon-blazer);
border-radius: 12px;
box-shadow: 4px 4px 0 var(--kon-cherry);
transform: rotate(-1.5deg);
&::before {
content: '♪';
margin-right: 10px;
color: var(--kon-cherry);
}
&::after {
content: '♫';
margin-left: 10px;
color: #3d6fc4;
}
&.text-muted {
color: #8a8aa3 !important;
border-color: #a9a9be;
box-shadow: 4px 4px 0 #cfcfdc;
&::before, &::after {
color: #a9a9be;
}
}
}
&[data-bs-theme="dark"] .day-navigator .title {
color: #f1f0ff !important;
background:
repeating-linear-gradient(180deg, transparent 0 11px, rgb(143 178 255 / 0.12) 11px 12px),
var(--luncher-bg-card);
border-color: #8fb2ff;
&::after {
color: #8fb2ff;
}
&.text-muted {
color: #8085b3 !important;
border-color: #4a5288;
box-shadow: 4px 4px 0 #2c3566;
}
}
// ---------- Karty podniků ----------
.restaurant-card {
border: 2px solid var(--kon-card-border);
border-radius: var(--luncher-radius-lg);
&:hover {
transform: translateY(-3px) rotate(-0.3deg);
border-color: var(--kon-cherry);
box-shadow: var(--luncher-shadow-lg);
}
.restaurant-header {
position: relative;
background:
#{$kon-members} bottom / 100% 4px no-repeat,
linear-gradient(90deg, var(--kon-blazer), var(--kon-blazer-light));
// Gitah: jantarový střed přechází do třešňové
&:hover {
background:
#{$kon-members} bottom / 100% 4px no-repeat,
radial-gradient(ellipse 80% 160% at 30% 50%, var(--kon-sunburst), var(--kon-cherry) 55%, #6e1530);
}
h3 {
font-weight: 900;
text-shadow: 0 2px 0 rgb(0 0 0 / 0.2);
}
small {
color: rgb(255 255 255 / 0.88);
}
}
}
.food-table tbody tr:hover > td {
background: #fdeee0;
}
&[data-bs-theme="dark"] .food-table tbody tr:hover > td {
background: #232a5c;
}
.food-price {
color: var(--kon-cherry);
font-weight: 700;
}
// ---------- Panely a tabulka voleb ----------
.choice-section,
.choices-table,
.pizza-section,
.no-votes {
border: 2px solid var(--kon-card-border);
border-radius: var(--luncher-radius-lg);
box-shadow: var(--luncher-shadow);
}
// <table> s border-collapse: collapse ignoruje border-radius — kulaté rohy jen se „separate"
.choices-table {
border-collapse: separate;
border-spacing: 0;
}
// Sloupec s podnikem: linkovaný papír se svislou okrajovou linkou jako v sešitě
.choices-table > tbody > tr > td:first-child {
background: #fdf1dc;
color: var(--kon-blazer);
font-weight: 700;
border-right: 2px solid rgb(196 51 88 / 0.45);
}
&[data-bs-theme="dark"] .choices-table > tbody > tr > td:first-child {
background: #1f2552;
color: #c9d6ff;
border-right-color: rgb(255 134 163 / 0.5);
}
// Bootstrap karty (skupinové objednávky apod.) a seznamy — stejné zaoblení a lem jako ostatní panely
.card {
border: 2px solid var(--kon-card-border);
border-radius: var(--luncher-radius-lg);
box-shadow: var(--luncher-shadow);
overflow: hidden;
background: var(--luncher-bg-card);
// Rozbalovací výběr z nabídky nesmí karta oříznout
&:has(.select-search-container) {
overflow: visible;
}
> .card-header {
background: var(--luncher-primary-light);
border-bottom: 2px solid var(--kon-card-border);
font-weight: 700;
&:first-child {
border-radius: calc(var(--luncher-radius-lg) - 2px) calc(var(--luncher-radius-lg) - 2px) 0 0;
}
}
// Průhledná tabulka — rohy pak zaobluje karta sama (i když nesmí ořezávat obsah)
.table {
--bs-table-bg: transparent;
margin-bottom: 0;
// Spodní linka posledního řádku by přečnívala přes zaoblené rohy
> :last-child > tr:last-child > * {
border-bottom-width: 0;
}
}
}
.list-group {
border-radius: var(--luncher-radius);
overflow: hidden;
border: 2px solid var(--kon-card-border);
> .list-group-item {
border-left: none;
border-right: none;
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
}
}
.form-select, .form-control,
input[type="text"], input[type="email"], input[type="password"] {
border-radius: var(--luncher-radius-sm);
border-width: 2px;
&:focus {
border-color: var(--kon-cherry);
box-shadow: 0 0 0 4px rgb(196 51 88 / 0.18);
}
}
// ---------- Tlačítka: zaoblené jako pedály efektů, s tmavě modrým „podstavcem" ----------
.btn {
border-radius: var(--luncher-radius-sm);
font-weight: 700;
letter-spacing: 0.2px;
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.2s ease;
&:active {
transform: translateY(3px) !important;
box-shadow: 0 1px 0 var(--kon-btn-shadow, transparent) !important;
}
}
.btn-primary {
--kon-btn-shadow: var(--kon-blazer-shadow);
background: linear-gradient(180deg, #e0567a, var(--kon-cherry-deep));
border: none;
color: #fff;
box-shadow: 0 4px 0 var(--kon-blazer);
text-shadow: 0 1px 0 rgb(0 0 0 / 0.2);
// Sunburst při najetí
&:hover {
background: radial-gradient(ellipse 90% 140% at 50% 40%, var(--kon-sunburst), var(--kon-cherry) 60%, var(--kon-cherry-deep));
box-shadow: 0 4px 0 var(--kon-blazer), 0 0 12px rgb(242 163 58 / 0.45);
}
&:disabled {
background: #c8c3cf;
box-shadow: 0 4px 0 #a5a1b0;
}
}
.btn-outline-primary {
--kon-btn-shadow: var(--kon-card-border);
background: var(--luncher-bg-card);
border: 2px solid var(--kon-blazer);
color: var(--kon-blazer);
box-shadow: 0 4px 0 var(--kon-card-border);
&:hover {
background: var(--kon-blazer);
color: #fff;
}
}
&[data-bs-theme="dark"] .btn-outline-primary {
border-color: #8fb2ff;
color: #c9d6ff;
&:hover {
background: #2b367a;
color: #fff;
}
}
.btn-secondary {
--kon-btn-shadow: var(--luncher-border);
background: var(--luncher-bg-card);
border: 2px solid var(--luncher-border);
color: var(--luncher-text);
box-shadow: 0 4px 0 var(--luncher-border);
}
.btn-danger {
--kon-btn-shadow: #8e1d33;
background: linear-gradient(180deg, #ef5a74, var(--luncher-danger));
border: none;
box-shadow: 0 4px 0 #8e1d33;
}
.action-icon:hover {
background: var(--luncher-primary-light);
color: var(--kon-cherry);
}
// ---------- Modaly ----------
.modal-content {
border: 2px solid var(--kon-card-border);
border-radius: var(--luncher-radius-xl);
overflow: hidden;
}
.modal-header {
background:
#{$kon-members} bottom / 100% 4px no-repeat,
linear-gradient(90deg, var(--kon-blazer-shadow), var(--kon-blazer) 40%, var(--kon-blazer-light));
border-bottom: none;
padding-bottom: calc(var(--bs-modal-header-padding-y, 1rem) + 4px);
.modal-title, .modal-title h2 {
color: #fff;
font-weight: 900;
text-shadow: 0 2px 0 rgb(0 0 0 / 0.2);
}
.btn-close {
filter: invert(1) grayscale(1) brightness(2);
}
}
.footer {
border-top: none;
background-image: #{$kon-members};
background-size: 100% 3px;
background-repeat: no-repeat;
background-position: top;
}
}
@keyframes kon-bounce {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-4px) rotate(-12deg); }
50% { transform: translateY(0) rotate(0deg); }
75% { transform: translateY(-2px) rotate(10deg); }
}
@media (prefers-reduced-motion: reduce) {
#{$kon} .navbar .navbar-brand::after {
animation: none;
}
}