Compare commits

2 Commits
Author SHA1 Message Date
timandClaude Opus 4.8 04185a0d97 Frontend: prepinac vzhladu kariet (znaky vs suit-icon)
Toggle v hlavicke prepina znaky farieb medzi textovymi glyfmi a suit-icon obrazkami; volba perzistuje v localStorage. Gula ma v icon rezime hnedy popis, stredove ikonky srdca/gule/listu jemne zmensene aby sa neprekryvali s rohmi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:37:57 +02:00
timandClaude Opus 4.8 97c71cf6e1 Frontend: rozmery a farby dosky centralizovane do boardTheme
Sizing konstanty (seat/standings/guess/totals) presunute do boardTheme.ts a farebne odlisenie tipu v PlayerCircle (nad tip = terakota).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:37:48 +02:00
20 changed files with 203 additions and 30 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,23 @@
import { useCardSkin } from '../lib/cardSkin';
/** Small header button that flips the deck between the classic vector cards and
* the illustrated "sedmové" art. Persists via the cardSkin store. */
export default function CardSkinToggle({ className = '' }: { className?: string }) {
const skin = useCardSkin((s) => s.skin);
const toggle = useCardSkin((s) => s.toggle);
const illustrated = skin === 'illustrated';
return (
<button
onClick={toggle}
title="Prepnúť vzhľad kariet"
aria-label="Prepnúť vzhľad kariet"
aria-pressed={illustrated}
className={`text-[13px] whitespace-nowrap transition-colors ${
illustrated ? 'text-gold hover:text-gold-bright' : 'text-[#7a7058] hover:text-gold'
} ${className}`}
>
{illustrated ? '🂠 Sedmové' : '🂠 Klasické'}
</button>
);
}
+41 -2
View File
@@ -1,4 +1,5 @@
import type { Card } from '../types'; import type { Card } from '../types';
import { useCardSkin, suitIcon } from '../lib/cardSkin';
const SUIT_SYMBOL: Record<string, string> = { const SUIT_SYMBOL: Record<string, string> = {
HEARTS: '♥', HEARTS: '♥',
@@ -19,6 +20,15 @@ const VALUE_LABEL: Record<string, string> = {
LOWER: 'J', UPPER: 'Q', KING: 'K', ACE: 'A', LOWER: 'J', UPPER: 'Q', KING: 'K', ACE: 'A',
}; };
// The bell, leaf and heart icons are a touch taller than the acorn, so in the
// centre they sometimes bleed into the corner marks — nudge those three down.
const CENTER_ICON_SCALE: Record<string, number> = {
HEARTS: 0.85,
LEAVES: 0.85,
BELLS: 0.85,
ACORNS: 1,
};
// Per-size geometry. sm/md sit on the table, lg/xl are hand cards. // Per-size geometry. sm/md sit on the table, lg/xl are hand cards.
const DIMS = { const DIMS = {
sm: { w: 38, h: 54, radius: 4, inset: 3, label: 9, suitSm: 7, suitLg: 18 }, sm: { w: 38, h: 54, radius: 4, inset: 3, label: 9, suitSm: 7, suitLg: 18 },
@@ -37,26 +47,45 @@ interface Props {
} }
export default function CardView({ card, onClick, disabled = false, highlight = false, size = 'md' }: Props) { export default function CardView({ card, onClick, disabled = false, highlight = false, size = 'md' }: Props) {
const skin = useCardSkin((s) => s.skin);
const symbol = SUIT_SYMBOL[card.color]; const symbol = SUIT_SYMBOL[card.color];
const color = SUIT_COLOR[card.color]; const color = SUIT_COLOR[card.color];
const label = VALUE_LABEL[card.value]; const label = VALUE_LABEL[card.value];
const d = DIMS[size]; const d = DIMS[size];
const interactive = !disabled && !!onClick; const interactive = !disabled && !!onClick;
// 'illustrated' skin keeps the classic card but swaps the ♥♠♣♦ glyphs for the
// suit-icon artwork; 'classic' uses the plain text glyphs.
const useIcons = skin === 'illustrated';
// With the icon artwork the bell suit reads brown, so tint its value label to
// match instead of the classic blue.
const labelColor = useIcons && card.color === 'BELLS' ? '#8a5a1c' : color;
const corner = (rotated: boolean) => ( const corner = (rotated: boolean) => (
<span <span
className="absolute flex flex-col items-center leading-none" // Icons align to the number's left edge so a wide label (VIII) doesn't shove
// the centred icon toward the middle; classic text stays centred as before.
className={`absolute flex flex-col leading-none ${useIcons ? 'items-start' : 'items-center'}`}
style={ style={
rotated rotated
? { bottom: d.inset, right: d.inset, transform: 'rotate(180deg)' } ? { bottom: d.inset, right: d.inset, transform: 'rotate(180deg)' }
: { top: d.inset, left: d.inset } : { top: d.inset, left: d.inset }
} }
> >
<span style={{ color, fontSize: d.label, fontFamily: 'Georgia,serif', fontWeight: 700, lineHeight: 1 }}> <span style={{ color: labelColor, fontSize: d.label, fontFamily: 'Georgia,serif', fontWeight: 700, lineHeight: 1 }}>
{label} {label}
</span> </span>
{useIcons ? (
<img
src={suitIcon(card.color, 'small')}
alt={symbol}
draggable={false}
style={{ height: d.suitSm * 1.25, width: 'auto', marginTop: 1 }}
className="select-none"
/>
) : (
<span style={{ color, fontSize: d.suitSm, lineHeight: 1.2 }}>{symbol}</span> <span style={{ color, fontSize: d.suitSm, lineHeight: 1.2 }}>{symbol}</span>
)}
</span> </span>
); );
@@ -76,7 +105,17 @@ export default function CardView({ card, onClick, disabled = false, highlight =
> >
{corner(false)} {corner(false)}
<span className="absolute inset-0 flex items-center justify-center"> <span className="absolute inset-0 flex items-center justify-center">
{useIcons ? (
<img
src={suitIcon(card.color, 'large')}
alt={symbol}
draggable={false}
style={{ height: d.suitLg * CENTER_ICON_SCALE[card.color], width: 'auto' }}
className="select-none"
/>
) : (
<span style={{ color, fontSize: d.suitLg, lineHeight: 1 }}>{symbol}</span> <span style={{ color, fontSize: d.suitLg, lineHeight: 1 }}>{symbol}</span>
)}
</span> </span>
{corner(true)} {corner(true)}
</button> </button>
+7 -3
View File
@@ -1,5 +1,6 @@
import { emit } from '../lib/socket'; import { emit } from '../lib/socket';
import { forbiddenGuess } from '../lib/gameRules'; import { forbiddenGuess } from '../lib/gameRules';
import { GUESS_BUTTON } from '../lib/boardTheme';
interface Props { interface Props {
cardsInRound: number; cardsInRound: number;
@@ -7,15 +8,16 @@ interface Props {
myOrder: number; myOrder: number;
activePlayer: number; activePlayer: number;
activePlayerName: string; activePlayerName: string;
desktop?: boolean;
} }
export default function GuessControls({ cardsInRound, guesses, myOrder, activePlayer, activePlayerName }: Props) { export default function GuessControls({ cardsInRound, guesses, myOrder, activePlayer, activePlayerName, desktop = true }: Props) {
const isMyTurn = activePlayer === myOrder; const isMyTurn = activePlayer === myOrder;
const forbidden = forbiddenGuess(cardsInRound, guesses); const forbidden = forbiddenGuess(cardsInRound, guesses);
if (!isMyTurn) { if (!isMyTurn) {
return ( return (
<p className="text-center text-sm text-green-dim py-3"> <p className="text-center text-[14px] text-green-dim py-3">
Čaká sa na tip:{' '} Čaká sa na tip:{' '}
<span className="font-serif text-gold">{activePlayerName}</span> <span className="font-serif text-gold">{activePlayerName}</span>
</p> </p>
@@ -23,6 +25,7 @@ export default function GuessControls({ cardsInRound, guesses, myOrder, activePl
} }
const options = Array.from({ length: cardsInRound + 1 }, (_, i) => i); const options = Array.from({ length: cardsInRound + 1 }, (_, i) => i);
const btn = desktop ? GUESS_BUTTON.desktop : GUESS_BUTTON.mobile;
return ( return (
<div className="flex flex-col items-center gap-3 py-3"> <div className="flex flex-col items-center gap-3 py-3">
@@ -36,8 +39,9 @@ export default function GuessControls({ cardsInRound, guesses, myOrder, activePl
disabled={isForbidden} disabled={isForbidden}
onClick={() => emit.addGuess(n)} onClick={() => emit.addGuess(n)}
title={isForbidden ? 'Zakázaná hodnota (súčet = počet kopiek)' : undefined} title={isForbidden ? 'Zakázaná hodnota (súčet = počet kopiek)' : undefined}
style={{ width: btn.size, height: btn.size, fontSize: btn.font }}
className={[ className={[
'w-11 h-11 rounded-full font-serif text-lg border-2 transition-colors', 'rounded-full font-serif border-2 transition-colors',
isForbidden isForbidden
? 'border-[#5a2a2a] text-[#7a4040] opacity-40 cursor-not-allowed' ? 'border-[#5a2a2a] text-[#7a4040] opacity-40 cursor-not-allowed'
: 'border-gold/50 text-gold hover:bg-gold hover:text-table hover:border-gold active:scale-95', : 'border-gold/50 text-gold hover:bg-gold hover:text-table hover:border-gold active:scale-95',
+9 -1
View File
@@ -12,6 +12,11 @@ interface Props {
export default function PlayerCircle({ name, won, guess, active, size = 52 }: Props) { export default function PlayerCircle({ name, won, guess, active, size = 52 }: Props) {
const nameFont = Math.max(9, Math.round(size * 0.17)); const nameFont = Math.max(9, Math.round(size * 0.17));
const valueFont = Math.round(size * 0.32); const valueFont = Math.round(size * 0.32);
// Bid status at a glance: terracotta once the player has taken more tricks
// than bid (the bid is already lost), gold while exactly on target, cream
// while still chasing. Scoring: exact match = 10 + bid, anything else = 0.
const guessColor =
guess === null ? '#d8cba6' : won > guess ? '#e08066' : won === guess ? '#e8c14a' : '#d8cba6';
// Oval: height a touch shorter than size so it reads as an ellipse. Width // Oval: height a touch shorter than size so it reads as an ellipse. Width
// starts at `size` (a circle for short names) but grows with the name via // starts at `size` (a circle for short names) but grows with the name via
// fit-content + padding, up to a cap beyond which the name is ellipsised // fit-content + padding, up to a cap beyond which the name is ellipsised
@@ -58,7 +63,10 @@ export default function PlayerCircle({ name, won, guess, active, size = 52 }: Pr
}} }}
> >
{won} {won}
<span style={{ fontSize: valueFont * 0.6, color: '#b0a585' }}>/{guess ?? '?'}</span> {/* The bid matters as much as the tricks won — keep it nearly as big
and in readable cream; only the slash separator stays muted. */}
<span style={{ fontSize: valueFont * 0.7, color: '#b0a585' }}>/</span>
<span style={{ fontSize: valueFont , color: guessColor }}>{guess ?? '?'}</span>
</span> </span>
</div> </div>
); );
+3 -8
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import type { PlayerInfo } from '../types'; import type { PlayerInfo } from '../types';
import { computeTotal } from '../lib/standings'; import { computeTotal } from '../lib/standings';
import { displayName } from '../lib/names'; import { displayName } from '../lib/names';
import { STANDINGS_FONT } from '../lib/boardTheme';
interface Props { interface Props {
standings: number[][][]; standings: number[][][];
@@ -40,14 +41,8 @@ export default function Standings({ standings, guesses = [], players, myOrder, d
const ROUNDS_PER_SERIES = 8; const ROUNDS_PER_SERIES = 8;
// Bigger, more legible type on the wide desktop sidebar; compact on mobile. // Bigger, more legible type on the wide desktop sidebar; compact on mobile.
const fz = { // Sizes live in one place (boardTheme.ts) alongside the rest of the board.
head: desktop ? 11 : 10, const fz = desktop ? STANDINGS_FONT.desktop : STANDINGS_FONT.mobile;
idx: desktop ? 13 : 9,
cell: desktop ? 19 : 14,
dot: desktop ? 18 : 13,
sigma: desktop ? 13 : 9,
total: desktop ? 20 : 18,
};
const gridCols = { gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` }; const gridCols = { gridTemplateColumns: `28px repeat(${cols.length}, 1fr)` };
+39
View File
@@ -0,0 +1,39 @@
// Single source of truth for the game board's fixed pixel sizes.
//
// The board deliberately uses fixed px (not rem) so it stays pixel-precise and
// scales as one unit via `useFitScale` (desktop) — see index.css for why the
// board opts out of the global rem type lever. These constants keep those px
// values in one place instead of scattered as inline literals across the board
// components (Standings / GameTable / …).
//
// PlayerCircle derives its own font/padding as ratios of the `size` it gets, so
// its internal ratios stay local to that component; only the per-seat `size`
// inputs live here.
/** Player-circle diameter per seat, by viewport. */
export const SEAT_SIZE = {
desktop: { top: 64, side: 64, me: 70 },
mobile: { top: 52, side: 48, me: 60 },
} as const;
/** Score list (Standings) type sizes. */
export const STANDINGS_FONT = {
desktop: { head: 12, idx: 13, cell: 18, dot: 18, sigma: 18, total: 20 },
mobile: { head: 11, idx: 9, cell: 16, dot: 13, sigma: 15, total: 18 },
} as const;
/** Header / score totals row (GameTable). The value size differs between the
* compact desktop header and the mobile header. */
export const TOTALS_FONT = {
label: 12,
valueDesktop: 18,
valueMobile: 17,
} as const;
/** Guess (tip) picker buttons (GuessControls): round button diameter + label
* size, in px — kept fixed like the rest of the board (not rem Tailwind classes)
* so they don't drift with the global font-size lever. */
export const GUESS_BUTTON = {
desktop: { size: 48, font: 22 },
mobile: { size: 40, font: 18 },
} as const;
+58
View File
@@ -0,0 +1,58 @@
import { create } from 'zustand';
import type { CardColor } from '../types';
// Two ways to draw the suit marks on the (otherwise identical) cards: plain
// ♥♠♣♦ text glyphs ('classic') or the "sedmové" suit-icon artwork ('illustrated').
export type CardSkin = 'classic' | 'illustrated';
const STORAGE_KEY = 'bridzik.cardSkin';
function loadSkin(): CardSkin {
try {
return localStorage.getItem(STORAGE_KEY) === 'illustrated' ? 'illustrated' : 'classic';
} catch {
return 'classic';
}
}
interface CardSkinStore {
skin: CardSkin;
setSkin: (skin: CardSkin) => void;
toggle: () => void;
}
export const useCardSkin = create<CardSkinStore>((set) => ({
skin: loadSkin(),
setSkin: (skin) => {
try {
localStorage.setItem(STORAGE_KEY, skin);
} catch {
/* ignore — a private-mode failure just means the choice isn't persisted */
}
set({ skin });
},
toggle: () => set((s) => {
const skin: CardSkin = s.skin === 'illustrated' ? 'classic' : 'illustrated';
try {
localStorage.setItem(STORAGE_KEY, skin);
} catch {
/* ignore */
}
return { skin };
}),
}));
// Filenames in /public/cards use their own suit/value words, not the engine's
// enum names — map between the two here.
const SUIT_FILE: Record<CardColor, string> = {
HEARTS: 'heart',
LEAVES: 'leaf',
ACORNS: 'acorn',
BELLS: 'bell',
};
/** Path to a suit icon at a given resolution, e.g. `/cards/suit-icons/heart-icon@small.png`.
* Used to render the suit marks on the classic vector cards. */
export function suitIcon(color: CardColor, size: 'small' | 'medium' | 'large'): string {
return `/cards/suit-icons/${SUIT_FILE[color]}-icon@${size}.png`;
}
+21 -14
View File
@@ -8,12 +8,14 @@ import { computeTotal } from '../lib/standings';
import { displayName } from '../lib/names'; import { displayName } from '../lib/names';
import { useIsDesktop } from '../lib/useIsDesktop'; import { useIsDesktop } from '../lib/useIsDesktop';
import { useFitScale } from '../lib/useFitScale'; import { useFitScale } from '../lib/useFitScale';
import { SEAT_SIZE, TOTALS_FONT } from '../lib/boardTheme';
import Hand from '../components/Hand'; import Hand from '../components/Hand';
import GuessControls from '../components/GuessControls'; import GuessControls from '../components/GuessControls';
import Trick from '../components/Trick'; import Trick from '../components/Trick';
import Standings from '../components/Standings'; import Standings from '../components/Standings';
import PlayerCircle from '../components/PlayerCircle'; import PlayerCircle from '../components/PlayerCircle';
import FaceDownCards from '../components/FaceDownCards'; import FaceDownCards from '../components/FaceDownCards';
import CardSkinToggle from '../components/CardSkinToggle';
import GameOver from './GameOver'; import GameOver from './GameOver';
import type { Hand as HandCards, PlayerInfo, StashData } from '../types'; import type { Hand as HandCards, PlayerInfo, StashData } from '../types';
@@ -205,19 +207,19 @@ export default function GameTable() {
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
{opponents.map((p) => ( {opponents.map((p) => (
<div key={p.order} className="text-center"> <div key={p.order} className="text-center">
<div className="uppercase tracking-[.1em] text-green-dim mb-0.5" style={{ fontSize: 11 }}> <div className="uppercase tracking-[.1em] text-green-dim mb-0.5" style={{ fontSize: TOTALS_FONT.label }}>
{displayName(p.name)} {displayName(p.name)}
</div> </div>
<div className="font-serif text-green-score leading-none" style={{ fontSize: compact ? 16 : 20 }}> <div className="font-serif text-green-score leading-none" style={{ fontSize: compact ? TOTALS_FONT.valueDesktop : TOTALS_FONT.valueMobile }}>
{computeTotal(standings, p.order)} {computeTotal(standings, p.order)}
</div> </div>
</div> </div>
))} ))}
<div className="text-center rounded-lg px-3 py-1 bg-gold/[.06] border border-gold/[.15]"> <div className="text-center rounded-lg px-3 py-1 bg-gold/[.06] border border-gold/[.15]">
<div className="uppercase tracking-[.1em] text-gold mb-0.5" style={{ fontSize: 11 }}> <div className="uppercase tracking-[.1em] text-gold mb-0.5" style={{ fontSize: TOTALS_FONT.label }}>
{myPlayer.name} {myPlayer.name}
</div> </div>
<div className="font-serif font-semibold text-gold-dim leading-none" style={{ fontSize: compact ? 16 : 20 }}> <div className="font-serif font-semibold text-gold-dim leading-none" style={{ fontSize: compact ? TOTALS_FONT.valueDesktop : TOTALS_FONT.valueMobile }}>
{computeTotal(standings, myOrder)} {computeTotal(standings, myOrder)}
</div> </div>
</div> </div>
@@ -241,27 +243,30 @@ export default function GameTable() {
myOrder={myOrder} myOrder={myOrder}
activePlayer={active_player} activePlayer={active_player}
activePlayerName={activePlayerName} activePlayerName={activePlayerName}
desktop={desktop}
/> />
) : null ) : null
); );
const seatSize = desktop ? SEAT_SIZE.desktop : SEAT_SIZE.mobile;
const topSeat = ( const topSeat = (
<div className="flex flex-col items-center gap-1.5"> <div className="flex flex-col items-center gap-1.5">
<PlayerCircle name={displayName(topP?.name) || '—'} {...seatProps(topP?.order)} size={desktop ? 64 : 52} /> <PlayerCircle name={displayName(topP?.name) || '—'} {...seatProps(topP?.order)} size={seatSize.top} />
<FaceDownCards count={cardsInHandOf(topP?.order)} direction="row" desktop={desktop} /> <FaceDownCards count={cardsInHandOf(topP?.order)} direction="row" desktop={desktop} />
</div> </div>
); );
const sideSeat = (p?: PlayerInfo) => ( const sideSeat = (p?: PlayerInfo) => (
<div className="flex flex-col items-center gap-1.5"> <div className="flex flex-col items-center gap-1.5">
<PlayerCircle name={displayName(p?.name) || '—'} {...seatProps(p?.order)} size={desktop ? 60 : 48} /> <PlayerCircle name={displayName(p?.name) || '—'} {...seatProps(p?.order)} size={seatSize.side} />
<FaceDownCards count={cardsInHandOf(p?.order)} direction="col" desktop={desktop} /> <FaceDownCards count={cardsInHandOf(p?.order)} direction="col" desktop={desktop} />
</div> </div>
); );
const meSeat = ( const meSeat = (
<div className="flex justify-center"> <div className="flex justify-center">
<PlayerCircle name={myPlayer.name} {...seatProps(myOrder)} size={desktop ? 70 : 58} /> <PlayerCircle name={myPlayer.name} {...seatProps(myOrder)} size={seatSize.me} />
</div> </div>
); );
@@ -283,22 +288,23 @@ export default function GameTable() {
<div className="flex-1 min-w-0 flex flex-col"> <div className="flex-1 min-w-0 flex flex-col">
{/* header */} {/* header */}
<div className="shrink-0 h-[58px] bg-header flex items-center gap-4 px-6 border-b border-[#14221a]"> <div className="shrink-0 h-[58px] bg-header flex items-center gap-4 px-6 border-b border-[#14221a]">
<span className="font-serif uppercase tracking-[.14em] text-[15px] text-gold whitespace-nowrap"> <span className="font-serif uppercase tracking-[.14em] text-[18px] text-gold whitespace-nowrap">
Bridžik Bridžik
</span> </span>
<div className="w-px h-[22px] bg-[#1a3a22]" /> <div className="w-px h-[22px] bg-[#1a3a22]" />
<span className="font-serif text-[12px] text-green-dim tracking-[.06em] whitespace-nowrap"> <span className="font-serif text-[14px] text-green-dim tracking-[.06em] whitespace-nowrap">
Séria {series_number + 1} · Kolo {round_number + 1} Séria {series_number + 1} · Kolo {round_number + 1}
</span> </span>
<div className="flex-1 flex items-center justify-center">{banner}</div> <div className="flex-1 flex items-center justify-center">{banner}</div>
{totalsRow(true)} {totalsRow(true)}
<div className="w-px h-[22px] bg-[#1a3a22]" /> <div className="w-px h-[22px] bg-[#1a3a22]" />
<CardSkinToggle />
{canEnd && ( {canEnd && (
<button onClick={handleEnd} className="text-[11px] text-[#8a8064] hover:text-gold whitespace-nowrap"> <button onClick={handleEnd} className="text-[13px] text-[#8a8064] hover:text-gold whitespace-nowrap">
Ukončiť Ukončiť
</button> </button>
)} )}
<button onClick={handleLeave} className="text-[11px] text-[#7a7058] hover:text-gold whitespace-nowrap"> <button onClick={handleLeave} className="text-[13px] text-[#7a7058] hover:text-gold whitespace-nowrap">
Odísť Odísť
</button> </button>
</div> </div>
@@ -344,16 +350,17 @@ export default function GameTable() {
{/* header */} {/* header */}
<div className="bg-header px-[18px] pt-[14px] pb-3 border-b border-[#14221a]"> <div className="bg-header px-[18px] pt-[14px] pb-3 border-b border-[#14221a]">
<div className="flex items-center justify-between mb-2.5"> <div className="flex items-center justify-between mb-2.5">
<span className="font-serif uppercase tracking-[.1em] text-[11px] text-gold"> <span className="font-serif uppercase tracking-[.1em] text-[12px] text-gold">
Séria {series_number + 1} · Kolo {round_number + 1} Séria {series_number + 1} · Kolo {round_number + 1}
</span> </span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<CardSkinToggle />
{canEnd && ( {canEnd && (
<button onClick={handleEnd} className="text-[11px] text-[#6a3030] hover:text-red-400"> <button onClick={handleEnd} className="text-[13px] text-[#6a3030] hover:text-red-400">
Ukončiť Ukončiť
</button> </button>
)} )}
<button onClick={handleLeave} className="text-[11px] text-[#7a7058] hover:text-gold"> <button onClick={handleLeave} className="text-[13px] text-[#7a7058] hover:text-gold">
Odísť Odísť
</button> </button>
</div> </div>