meow
This commit is contained in:
+200
-41
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
|
||||
import FallingLeaves from '../Tree/FallingLeaves'
|
||||
import ReplayCanvasPanel from './ReplayCanvas'
|
||||
@@ -3753,9 +3753,14 @@ function TournamentsPage({ navigate }) {
|
||||
type="button"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-lg font-semibold">
|
||||
{tournament.name || `Tournament ${tournament.tournament_id}`}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<p className="truncate text-lg font-semibold">
|
||||
{tournament.name || `Tournament ${tournament.tournament_id}`}
|
||||
</p>
|
||||
<span className="shrink-0 rounded bg-surface px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-fury-violet">
|
||||
{tournamentFormatMeta(tournament.format).label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-soft">
|
||||
TID {tournament.tournament_id} · {tournamentDateRange(tournament.date_start, tournament.date_end)}
|
||||
</p>
|
||||
@@ -3776,37 +3781,74 @@ function TournamentsPage({ navigate }) {
|
||||
)
|
||||
}
|
||||
|
||||
function TournamentMatchCard({ match, navigate }) {
|
||||
function TournamentMatchCard({ match, navigate, onHover }) {
|
||||
const winner = displayTeamName(match.winner_name).toLowerCase()
|
||||
const teamA = displayTeamName(match.team_a_name)
|
||||
const teamB = displayTeamName(match.team_b_name)
|
||||
const aWon = winner && teamA && winner === teamA.toLowerCase()
|
||||
const bWon = winner && teamB && winner === teamB.toLowerCase()
|
||||
const decided = Boolean(winner)
|
||||
const battles = Array.isArray(match.battles) ? match.battles : []
|
||||
const fire = onHover || (() => {})
|
||||
const lastKey = useRef('')
|
||||
|
||||
const teamRow = (name, score, won, emptyLabel = 'TBD') => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{name ? (
|
||||
<button
|
||||
className={`min-w-0 truncate text-left font-semibold transition hover:underline ${won ? 'text-win' : 'text-text'}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
navigate(teamPath(name))
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
) : (
|
||||
<span className="min-w-0 truncate font-semibold text-text-muted">{emptyLabel}</span>
|
||||
)}
|
||||
<span className={`shrink-0 tabular-nums ${won ? 'text-win' : 'text-text-soft'}`}>{formatNumber(score)}</span>
|
||||
</div>
|
||||
)
|
||||
// Hover a team name → light just that team's run (green). Hover anywhere else on
|
||||
// the card → light both teams' runs (winner green, loser red). A registered team
|
||||
// that forfeited (technical walkover) shows gold rather than red.
|
||||
const matchHighlight = () => {
|
||||
const map = {}
|
||||
if (teamA) map[teamA.toLowerCase()] = !decided || aWon ? 'win' : 'loss'
|
||||
if (teamB) map[teamB.toLowerCase()] = !decided || bWon ? 'win' : 'loss'
|
||||
return Object.keys(map).length ? map : null
|
||||
}
|
||||
const handleOver = (event) => {
|
||||
const teamEl = event.target.closest('[data-team]')
|
||||
const map = teamEl ? { [teamEl.dataset.team.toLowerCase()]: 'win' } : matchHighlight()
|
||||
const key = map ? Object.entries(map).map(([k, v]) => `${k}:${v}`).sort().join('|') : ''
|
||||
if (key === lastKey.current) return
|
||||
lastKey.current = key
|
||||
fire(map)
|
||||
}
|
||||
const handleLeave = () => {
|
||||
lastKey.current = ''
|
||||
fire(null)
|
||||
}
|
||||
|
||||
const teamRow = (name, score, won, emptyLabel = 'TBD') => {
|
||||
const lost = decided && name && !won
|
||||
const noShow = lost && match.status === 'technical'
|
||||
const nameColor = won ? 'text-win' : noShow ? 'text-fury-violet' : lost ? 'text-loss' : 'text-text'
|
||||
const scoreColor = won ? 'text-win' : noShow ? 'text-fury-violet' : lost ? 'text-loss' : 'text-text-soft'
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{name ? (
|
||||
<button
|
||||
className={`min-w-0 truncate text-left font-semibold transition hover:underline ${nameColor}`}
|
||||
data-team={name}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
if (bracketPan.dragged) return
|
||||
navigate(teamPath(name))
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
) : (
|
||||
<span className="min-w-0 truncate font-semibold text-text-muted">{emptyLabel}</span>
|
||||
)}
|
||||
<span className={`shrink-0 tabular-nums ${scoreColor}`}>{formatNumber(score)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const emptyLabel = match.status === 'bye' ? 'BYE' : 'TBD'
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-bg p-2.5 text-sm shadow-sm">
|
||||
<div
|
||||
className="rounded-md border border-border bg-bg p-2.5 text-sm shadow-sm"
|
||||
onMouseLeave={handleLeave}
|
||||
onMouseOver={handleOver}
|
||||
>
|
||||
{teamRow(teamA, match.score_a, aWon, emptyLabel)}
|
||||
<div className="mt-1">{teamRow(teamB, match.score_b, bWon, emptyLabel)}</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2 text-[10px] font-semibold uppercase tracking-wide text-text-muted">
|
||||
@@ -3820,7 +3862,11 @@ function TournamentMatchCard({ match, navigate }) {
|
||||
<button
|
||||
className="rounded bg-surface px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-text-soft transition hover:text-text"
|
||||
key={battle.session_hex}
|
||||
onClick={() => navigate(gamePath(battle.session_hex))}
|
||||
onClick={() => {
|
||||
if (bracketPan.dragged) return
|
||||
navigate(gamePath(battle.session_hex))
|
||||
}}
|
||||
title={`Game ${index + 1} — view replay`}
|
||||
type="button"
|
||||
>
|
||||
G{index + 1}
|
||||
@@ -3829,6 +3875,7 @@ function TournamentMatchCard({ match, navigate }) {
|
||||
<span
|
||||
className="rounded bg-surface px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-text-muted opacity-70"
|
||||
key={battle.session_hex}
|
||||
title={`Game ${index + 1} — no replay held`}
|
||||
>
|
||||
G{index + 1}
|
||||
</span>
|
||||
@@ -3840,12 +3887,80 @@ function TournamentMatchCard({ match, navigate }) {
|
||||
)
|
||||
}
|
||||
|
||||
function TournamentBracketSide({ side, navigate }) {
|
||||
// Shared across the bracket: set true while a pan-drag is in progress so the
|
||||
// team/replay buttons inside the canvas don't fire a navigation on release.
|
||||
const bracketPan = { dragged: false }
|
||||
|
||||
function teamsOf(match) {
|
||||
return [match?.team_a_name, match?.team_b_name]
|
||||
.map((n) => displayTeamName(n).toLowerCase())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
// Drag-anywhere pan surface. A tap still clicks; only motion past a small
|
||||
// threshold pans (and suppresses the click). Move/up live on the window so a fast
|
||||
// drag keeps panning even when the cursor leaves the canvas.
|
||||
function BracketViewport({ children }) {
|
||||
const ref = useRef(null)
|
||||
|
||||
const onPointerDown = (event) => {
|
||||
const el = ref.current
|
||||
if (!el || event.button !== 0) return
|
||||
const start = { x: event.clientX, y: event.clientY, sl: el.scrollLeft, st: el.scrollTop, moved: false }
|
||||
bracketPan.dragged = false
|
||||
|
||||
const onMove = (e) => {
|
||||
const dx = e.clientX - start.x
|
||||
const dy = e.clientY - start.y
|
||||
if (!start.moved && Math.hypot(dx, dy) > 5) {
|
||||
start.moved = true
|
||||
bracketPan.dragged = true
|
||||
el.classList.add('is-grabbing')
|
||||
}
|
||||
if (start.moved) {
|
||||
el.scrollLeft = start.sl - dx
|
||||
el.scrollTop = start.st - dy
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
const onUp = () => {
|
||||
el.classList.remove('is-grabbing')
|
||||
window.removeEventListener('pointermove', onMove)
|
||||
window.removeEventListener('pointerup', onUp)
|
||||
// Keep `dragged` true through the click that fires right after pointerup so
|
||||
// that click is suppressed, then clear it.
|
||||
window.setTimeout(() => { bracketPan.dragged = false }, 0)
|
||||
}
|
||||
window.addEventListener('pointermove', onMove)
|
||||
window.addEventListener('pointerup', onUp)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bracket-viewport" onPointerDown={onPointerDown} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// What colour, if any, a team should glow: the highlight map is { teamLower:
|
||||
// 'win' | 'loss' }. Winner-side runs glow green, loser runs red.
|
||||
function traceClass(teams, highlight) {
|
||||
if (!highlight) return ''
|
||||
let result = ''
|
||||
for (const team of teams) {
|
||||
const tone = highlight[team]
|
||||
if (tone === 'win') return ' is-traced'
|
||||
if (tone === 'loss') result = ' is-traced-loss'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function TournamentBracketSide({ side, navigate, highlight, onHover }) {
|
||||
const gridRef = useRef(null)
|
||||
const nodeRefs = useRef(new Map())
|
||||
// `tops` places each match; once placed we read back the DOM to draw connectors.
|
||||
const [layout, setLayout] = useState({ tops: new Map(), height: 0 })
|
||||
const [connectors, setConnectors] = useState({ width: 0, height: 0, paths: [] })
|
||||
const [connectors, setConnectors] = useState({ width: 0, height: 0, lines: [], byes: [] })
|
||||
|
||||
// Pass 1: measure card heights, then position every match centred on its feeders.
|
||||
useLayoutEffect(() => {
|
||||
@@ -3864,6 +3979,9 @@ function TournamentBracketSide({ side, navigate }) {
|
||||
}, [side])
|
||||
|
||||
// Pass 2: with matches positioned, read real edges and draw elbow connectors.
|
||||
// Each connector carries the teams on either end so a hovered team's whole run
|
||||
// can be lit. Matches with no incoming connector (all feeders were byes) get a
|
||||
// bye stub so they don't read as appearing from nowhere.
|
||||
useLayoutEffect(() => {
|
||||
const grid = gridRef.current
|
||||
if (!grid) return
|
||||
@@ -3878,7 +3996,8 @@ function TournamentBracketSide({ side, navigate }) {
|
||||
mid: b.top - gridBox.top + b.height / 2,
|
||||
}
|
||||
}
|
||||
const paths = []
|
||||
const lines = []
|
||||
const fed = new Set()
|
||||
for (let c = 0; c < side.columns.length - 1; c += 1) {
|
||||
for (const match of side.columns[c].matches) {
|
||||
const parent = feederParent(match, c, side.columns)
|
||||
@@ -3886,26 +4005,54 @@ function TournamentBracketSide({ side, navigate }) {
|
||||
const from = boxOf(layoutKey(c, match))
|
||||
const to = boxOf(layoutKey(c + 1, parent))
|
||||
if (!from || !to) continue
|
||||
fed.add(parent.match_id)
|
||||
const midX = (from.right + to.left) / 2
|
||||
paths.push(`M ${from.right} ${from.mid} H ${midX} V ${to.mid} H ${to.left}`)
|
||||
lines.push({
|
||||
d: `M ${from.right} ${from.mid} H ${midX} V ${to.mid} H ${to.left}`,
|
||||
teams: teamsOf(match).filter((t) => teamsOf(parent).includes(t)),
|
||||
})
|
||||
}
|
||||
}
|
||||
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, paths })
|
||||
const byes = []
|
||||
for (let c = 1; c < side.columns.length; c += 1) {
|
||||
for (const match of side.columns[c].matches) {
|
||||
if (fed.has(match.match_id)) continue
|
||||
const box = boxOf(layoutKey(c, match))
|
||||
if (box) byes.push({ x: box.left, y: box.mid })
|
||||
}
|
||||
}
|
||||
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, lines, byes })
|
||||
}, [side, layout])
|
||||
|
||||
const active = Boolean(highlight)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-fury-cyan">{side.label}</h3>
|
||||
<div className="overflow-x-auto pb-2">
|
||||
<div className="tournament-bracket-grid" ref={gridRef}>
|
||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-fury-violet">{side.label}</h3>
|
||||
<BracketViewport>
|
||||
<div className={`tournament-bracket-grid${active ? ' has-trace' : ''}`} ref={gridRef}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="tournament-bracket-lines"
|
||||
width={connectors.width}
|
||||
height={connectors.height}
|
||||
width={connectors.width}
|
||||
>
|
||||
{connectors.paths.map((d, index) => (
|
||||
<path d={d} key={index} />
|
||||
{connectors.lines.map((line, index) => {
|
||||
const lit = traceClass(line.teams, highlight)
|
||||
return (
|
||||
<g key={index}>
|
||||
<path className={`bracket-line-base${lit}`} d={line.d} />
|
||||
<path className={`bracket-line-flow${lit}`} d={line.d} />
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
{connectors.byes.map((bye, index) => (
|
||||
<g key={`bye-${index}`}>
|
||||
<path className="bracket-bye-stub" d={`M ${bye.x} ${bye.y} h -18`} />
|
||||
<text className="bracket-bye-tag" textAnchor="end" x={bye.x - 24} y={bye.y + 3}>
|
||||
bye
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
{side.columns.map((column, columnIndex) => (
|
||||
@@ -3916,9 +4063,10 @@ function TournamentBracketSide({ side, navigate }) {
|
||||
<div className="tournament-round-track" style={{ height: layout.height || undefined }}>
|
||||
{column.matches.map((match) => {
|
||||
const key = layoutKey(columnIndex, match)
|
||||
const traced = traceClass(teamsOf(match), highlight)
|
||||
return (
|
||||
<div
|
||||
className="tournament-match-node"
|
||||
className={`tournament-match-node${traced}`}
|
||||
key={`${match.type_bracket}-${match.match_id}`}
|
||||
ref={(el) => {
|
||||
if (el) nodeRefs.current.set(key, el)
|
||||
@@ -3926,7 +4074,7 @@ function TournamentBracketSide({ side, navigate }) {
|
||||
}}
|
||||
style={{ top: layout.tops.get(key) ?? 0 }}
|
||||
>
|
||||
<TournamentMatchCard match={match} navigate={navigate} />
|
||||
<TournamentMatchCard match={match} navigate={navigate} onHover={onHover} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -3934,7 +4082,7 @@ function TournamentBracketSide({ side, navigate }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</BracketViewport>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4037,6 +4185,11 @@ function TournamentDetailPage({ tournamentId, navigate }) {
|
||||
const listSides = sides.filter((side) => side.kind === 'list')
|
||||
const standings = data?.standings || []
|
||||
const hasStandings = standings.length > 0
|
||||
// Lit-up teams shared across both brackets, so a run lights in the winner
|
||||
// bracket and wherever it dropped into the loser bracket at once. Shape:
|
||||
// { teamLower: 'win' | 'loss' } or null.
|
||||
const [highlight, setHighlight] = useState(null)
|
||||
const onHover = useCallback((map) => setHighlight(map), [])
|
||||
|
||||
return (
|
||||
<section className="space-y-6 pt-24 sm:pt-28">
|
||||
@@ -4097,7 +4250,13 @@ function TournamentDetailPage({ tournamentId, navigate }) {
|
||||
<h2 className="text-lg font-semibold">Playoffs</h2>
|
||||
) : null}
|
||||
{bracketSides.map((side) => (
|
||||
<TournamentBracketSide key={side.key} side={side} navigate={navigate} />
|
||||
<TournamentBracketSide
|
||||
highlight={highlight}
|
||||
key={side.key}
|
||||
navigate={navigate}
|
||||
onHover={onHover}
|
||||
side={side}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
+136
-3
@@ -831,11 +831,39 @@ h3 {
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
/* The bracket reads as a campaign map you drag around. The viewport breaks the
|
||||
page's max-width so there's room to manoeuvre; the surface carries a faint
|
||||
tactical grid that pans with the bracket (it lives on the scrolling content). */
|
||||
.bracket-viewport {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
margin-left: calc(50% - 50vw);
|
||||
max-height: 78vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
cursor: grab;
|
||||
border-block: 1px solid var(--color-border);
|
||||
background-color: color-mix(in srgb, var(--color-bg) 86%, #000);
|
||||
}
|
||||
|
||||
.bracket-viewport.is-grabbing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tournament-bracket-grid {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 2.6rem;
|
||||
min-width: max-content;
|
||||
padding: 3rem clamp(1.5rem, 5vw, 5rem);
|
||||
--grid-line: color-mix(in srgb, var(--color-border) 38%, transparent);
|
||||
--grid-dot: color-mix(in srgb, var(--color-fury-violet) 16%, transparent);
|
||||
background-image:
|
||||
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
|
||||
radial-gradient(var(--grid-dot) 1.5px, transparent 1.6px);
|
||||
background-size: 116px 116px, 116px 116px, 116px 116px;
|
||||
background-position: 0 0, 0 0, 58px 58px;
|
||||
}
|
||||
|
||||
/* Connector layer: absolutely sized to the full bracket, drawn behind the cards.
|
||||
@@ -850,18 +878,55 @@ h3 {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Each connector is two stacked strokes: a steady base rail plus a brighter
|
||||
dashed tracer that flows toward the winner (left → right, the path's own
|
||||
direction). */
|
||||
.tournament-bracket-lines path {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--color-border) 60%, var(--color-fury-violet));
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.bracket-line-base {
|
||||
stroke: color-mix(in srgb, var(--color-border) 55%, var(--color-fury-violet));
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.bracket-line-flow {
|
||||
stroke: color-mix(in srgb, var(--color-fury-violet) 92%, white);
|
||||
stroke-width: 1.5px;
|
||||
stroke-dasharray: 7 17;
|
||||
opacity: 0.7;
|
||||
animation: bracketFlow 1.4s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes bracketFlow {
|
||||
to {
|
||||
stroke-dashoffset: -24;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bye stub: a short rail + tag on the incoming side of a match whose feeders
|
||||
were all byes, so a top-seed match doesn't read as "appearing from nowhere". */
|
||||
.bracket-bye-stub {
|
||||
stroke: color-mix(in srgb, var(--color-text-muted) 60%, transparent);
|
||||
stroke-width: 1.5px;
|
||||
stroke-dasharray: 3 4;
|
||||
}
|
||||
|
||||
.bracket-bye-tag {
|
||||
fill: var(--color-text-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tournament-round-column {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 190px;
|
||||
min-width: 190px;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -878,6 +943,74 @@ h3 {
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
/* Hover a team → its whole run lights up; everything else recedes. */
|
||||
.tournament-bracket-grid.has-trace .tournament-match-node:not(.is-traced):not(.is-traced-loss) {
|
||||
opacity: 0.32;
|
||||
}
|
||||
|
||||
.tournament-match-node.is-traced > * {
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1.5px var(--color-win), 0 0 18px -2px color-mix(in srgb, var(--color-win) 70%, transparent);
|
||||
animation: bracketBreathe 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bracketBreathe {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--color-win) 70%, transparent), 0 0 14px -4px color-mix(in srgb, var(--color-win) 55%, transparent);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 2px var(--color-win), 0 0 26px 0 color-mix(in srgb, var(--color-win) 75%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.bracket-line-base.is-traced {
|
||||
stroke: var(--color-win);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.bracket-line-flow.is-traced {
|
||||
stroke: color-mix(in srgb, var(--color-win) 75%, white);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Loser run: same treatment in the loss colour. */
|
||||
.tournament-match-node.is-traced-loss > * {
|
||||
position: relative;
|
||||
animation: bracketBreatheLoss 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bracketBreatheLoss {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--color-loss) 70%, transparent), 0 0 14px -4px color-mix(in srgb, var(--color-loss) 55%, transparent);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 2px var(--color-loss), 0 0 26px 0 color-mix(in srgb, var(--color-loss) 75%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.bracket-line-base.is-traced-loss {
|
||||
stroke: var(--color-loss);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.bracket-line-flow.is-traced-loss {
|
||||
stroke: color-mix(in srgb, var(--color-loss) 75%, white);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.tournament-bracket-grid.has-trace .tournament-bracket-lines path:not(.is-traced):not(.is-traced-loss) {
|
||||
opacity: 0.18;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bracket-line-flow,
|
||||
.tournament-match-node.is-traced > *,
|
||||
.tournament-match-node.is-traced-loss > * {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scrollPulse {
|
||||
|
||||
Reference in New Issue
Block a user