Compare commits

...

8 Commits

Author SHA1 Message Date
Anton
42057ddb1c chore: fix versioning
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
2026-04-27 14:20:05 +03:00
1a37afd16f Merge pull request 'fix(frontend): prevent calendar loading layout shift' (#29) from fix/calendar-loading-layout-shift into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Reviewed-on: #29
2026-04-27 11:03:46 +00:00
Anton
f7b611bbbe fix(frontend): prevent calendar loading layout shift
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
2026-04-27 14:02:20 +03:00
55fc23ec64 Merge pull request 'fix frontend calendar race states' (#28) from codex/calendar-race-ui-fixes into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Reviewed-on: #28
2026-04-27 09:32:39 +00:00
Anton
dffbb48d99 fix frontend calendar race states
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
2026-04-27 12:31:29 +03:00
0b7ad23252 Merge pull request 'chore: resizes images' (#27) from chore/resize-images into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Reviewed-on: #27
2026-04-22 10:27:38 +00:00
Anton
19e9e59125 chore: resizes images
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
2026-04-22 12:50:39 +03:00
bfbbaeae59 Merge pull request 'feat(frontend): redesign race dashboard' (#26) from feature/sport-dashboard-redesign into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Reviewed-on: #26
2026-04-22 08:48:30 +00:00
20 changed files with 249 additions and 64 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"version": "0.5.0", "version": "0.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"version": "0.5.0", "version": "0.5.1",
"dependencies": { "dependencies": {
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@@ -1,7 +1,7 @@
{ {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"private": true, "private": true,
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -1,11 +1,13 @@
import { NavLink, Outlet } from "react-router-dom"; import { Link, NavLink, Outlet } from "react-router-dom";
import { AppShellFooter } from "./AppShellFooter"; import { AppShellFooter } from "./AppShellFooter";
export function AppLayout(): JSX.Element { export function AppLayout(): JSX.Element {
return ( return (
<div className="app-shell"> <div className="app-shell">
<header className="app-shell__header"> <header className="app-shell__header">
<div className="app-shell__brand">Календарь стартов</div> <Link className="app-shell__brand" to="/">
Календарь стартов
</Link>
<nav className="app-shell__nav" aria-label="Основная навигация"> <nav className="app-shell__nav" aria-label="Основная навигация">
<NavLink <NavLink
to="/" to="/"

View File

@@ -1,14 +1,14 @@
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import type { Race } from "../api"; import type { Race } from "../api";
import { buildMonthCells, groupRacesByYmd, toYmd, WEEKDAY_LABELS_SHORT_RU } from "../lib"; import { buildMonthCells, groupRacesByYmd, isRaceDateInPast, toYmd, WEEKDAY_LABELS_SHORT_RU } from "../lib";
const MONTH_NAMES_RU_SHORT = [ const MONTH_NAMES_RU_SHORT = [
"янв.", "янв.",
"февр.", "февр.",
"мар.", "мар.",
"апр.", "апр.",
"мая", "май",
"июн.", "июн.",
"июл.", "июл.",
"авг.", "авг.",
@@ -20,6 +20,13 @@ const MONTH_NAMES_RU_SHORT = [
const POPOVER_LEAVE_MS = 140; const POPOVER_LEAVE_MS = 140;
function toLocalYmd(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
interface RacesCalendarProps { interface RacesCalendarProps {
displayYear: number; displayYear: number;
monthFilter: string; monthFilter: string;
@@ -77,6 +84,8 @@ function CalendarMonthBlock(props: {
setOpenYmd: (v: string | null) => void; setOpenYmd: (v: string | null) => void;
scheduleClose: () => void; scheduleClose: () => void;
cancelClose: () => void; cancelClose: () => void;
onMonthSelect?: (monthIndex: number) => void;
todayYmd: string;
}): JSX.Element { }): JSX.Element {
const { const {
year, year,
@@ -88,6 +97,8 @@ function CalendarMonthBlock(props: {
setOpenYmd, setOpenYmd,
scheduleClose, scheduleClose,
cancelClose, cancelClose,
onMonthSelect,
todayYmd,
} = props; } = props;
const cells = useMemo(() => buildMonthCells(year, monthIndex), [year, monthIndex]); const cells = useMemo(() => buildMonthCells(year, monthIndex), [year, monthIndex]);
const title = `${MONTH_NAMES_RU_SHORT[monthIndex]} ${year}`; const title = `${MONTH_NAMES_RU_SHORT[monthIndex]} ${year}`;
@@ -96,7 +107,21 @@ function CalendarMonthBlock(props: {
return ( return (
<div className={blockClass}> <div className={blockClass}>
<h3 className="races-cal__month-title">{title}</h3> <h3 className="races-cal__month-title">
{onMonthSelect ? (
<button
type="button"
className="races-cal__month-title-button"
onClick={() => {
onMonthSelect(monthIndex);
}}
>
{title}
</button>
) : (
title
)}
</h3>
<div className="races-cal__weekdays" aria-hidden> <div className="races-cal__weekdays" aria-hidden>
{WEEKDAY_LABELS_SHORT_RU.map((d) => ( {WEEKDAY_LABELS_SHORT_RU.map((d) => (
<span key={d} className="races-cal__weekday"> <span key={d} className="races-cal__weekday">
@@ -113,11 +138,22 @@ function CalendarMonthBlock(props: {
const dayRaces = racesByYmd.get(ymd) ?? []; const dayRaces = racesByYmd.get(ymd) ?? [];
const hasRaces = dayRaces.length > 0; const hasRaces = dayRaces.length > 0;
const isOpen = openYmd === ymd; const isOpen = openYmd === ymd;
const isPast = isRaceDateInPast(ymd);
const isToday = ymd === todayYmd;
const cellClassName = [
"races-cal__cell",
hasRaces ? "races-cal__cell--has-race" : "",
isOpen ? "races-cal__cell--open" : "",
isPast ? "races-cal__cell--past" : "",
isToday ? "races-cal__cell--today" : "",
]
.filter(Boolean)
.join(" ");
return ( return (
<div <div
key={ymd} key={ymd}
className={`races-cal__cell${hasRaces ? " races-cal__cell--has-race" : ""}${isOpen ? " races-cal__cell--open" : ""}`} className={cellClassName}
onMouseEnter={() => { onMouseEnter={() => {
cancelClose(); cancelClose();
setOpenYmd(hasRaces ? ymd : null); setOpenYmd(hasRaces ? ymd : null);
@@ -182,6 +218,7 @@ export function RacesCalendar(props: RacesCalendarProps): JSX.Element {
}, [cancelClose]); }, [cancelClose]);
const racesByYmd = useMemo(() => groupRacesByYmd(races), [races]); const racesByYmd = useMemo(() => groupRacesByYmd(races), [races]);
const todayYmd = useMemo(() => toLocalYmd(new Date()), []);
const focusedMonthIndex = monthFilter === "" ? null : parseInt(monthFilter, 10) - 1; const focusedMonthIndex = monthFilter === "" ? null : parseInt(monthFilter, 10) - 1;
@@ -202,6 +239,11 @@ export function RacesCalendar(props: RacesCalendarProps): JSX.Element {
setOpenYmd={setOpenYmd} setOpenYmd={setOpenYmd}
scheduleClose={scheduleClose} scheduleClose={scheduleClose}
cancelClose={cancelClose} cancelClose={cancelClose}
onMonthSelect={(mi) => {
onMonthFilterChange(String(mi + 1));
setOpenYmd(null);
}}
todayYmd={todayYmd}
/> />
))} ))}
</div> </div>
@@ -240,6 +282,7 @@ export function RacesCalendar(props: RacesCalendarProps): JSX.Element {
setOpenYmd={setOpenYmd} setOpenYmd={setOpenYmd}
scheduleClose={scheduleClose} scheduleClose={scheduleClose}
cancelClose={cancelClose} cancelClose={cancelClose}
todayYmd={todayYmd}
/> />
</div> </div>
)} )}

View File

@@ -21,36 +21,36 @@ interface OfficialRaceVisual {
const FALLBACK_VISUALS: Record<RaceVisualVariant, RaceVisual> = { const FALLBACK_VISUALS: Record<RaceVisualVariant, RaceVisual> = {
short: { short: {
variant: "short", variant: "short",
imageSrc: "/images/race-short.png", imageSrc: "/images/race-short.jpg",
fallbackSrc: "/images/race-short.png", fallbackSrc: "/images/race-short.jpg",
imageFit: "cover", imageFit: "cover",
label: "Городской темп", label: "Городской темп",
}, },
half: { half: {
variant: "half", variant: "half",
imageSrc: "/images/race-half.png", imageSrc: "/images/race-half.jpg",
fallbackSrc: "/images/race-half.png", fallbackSrc: "/images/race-half.jpg",
imageFit: "cover", imageFit: "cover",
label: "Полумарафон", label: "Полумарафон",
}, },
marathon: { marathon: {
variant: "marathon", variant: "marathon",
imageSrc: "/images/race-marathon.png", imageSrc: "/images/race-marathon.jpg",
fallbackSrc: "/images/race-marathon.png", fallbackSrc: "/images/race-marathon.jpg",
imageFit: "cover", imageFit: "cover",
label: "Марафон", label: "Марафон",
}, },
trail: { trail: {
variant: "trail", variant: "trail",
imageSrc: "/images/race-trail.png", imageSrc: "/images/race-trail.jpg",
fallbackSrc: "/images/race-trail.png", fallbackSrc: "/images/race-trail.jpg",
imageFit: "cover", imageFit: "cover",
label: "Трейл", label: "Трейл",
}, },
night: { night: {
variant: "night", variant: "night",
imageSrc: "/images/race-night.png", imageSrc: "/images/race-night.jpg",
fallbackSrc: "/images/race-night.png", fallbackSrc: "/images/race-night.jpg",
imageFit: "cover", imageFit: "cover",
label: "Ночной старт", label: "Ночной старт",
}, },

View File

@@ -93,6 +93,17 @@ function validateForm(form: FormData): string[] {
return errors; return errors;
} }
function isRaceDateTodayOrPast(date: string): boolean {
if (!date.trim()) {
return false;
}
const today = new Date();
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, "0");
const d = String(today.getDate()).padStart(2, "0");
return isRaceDateInPast(date) || date.slice(0, 10) === `${y}-${m}-${d}`;
}
export function RaceFormPage(): JSX.Element { export function RaceFormPage(): JSX.Element {
const { raceId } = useParams<{ raceId: string }>(); const { raceId } = useParams<{ raceId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -243,6 +254,7 @@ export function RaceFormPage(): JSX.Element {
); );
const hideOrgScheduleFields = isEditMode && isRaceDateInPast(form.date); const hideOrgScheduleFields = isEditMode && isRaceDateInPast(form.date);
const showResultFields = isRaceDateTodayOrPast(form.date);
const pageTitle = isEditMode ? "Редактирование старта" : "Новый старт"; const pageTitle = isEditMode ? "Редактирование старта" : "Новый старт";
if (isLoading) { if (isLoading) {
@@ -397,33 +409,35 @@ export function RaceFormPage(): JSX.Element {
</label> </label>
</fieldset> </fieldset>
<fieldset className="race-form__group"> {showResultFields ? (
<legend className="race-form__legend">Результаты</legend> <fieldset className="race-form__group">
<legend className="race-form__legend">Результаты</legend>
<label className="race-form__field"> <label className="race-form__field">
<span className="race-form__label">Финишное время</span> <span className="race-form__label">Финишное время</span>
<input <input
className="race-form__input" className="race-form__input"
type="text" type="text"
name="finishTime" name="finishTime"
value={form.finishTime} value={form.finishTime}
onChange={handleChange} onChange={handleChange}
placeholder="1:45:30" placeholder="1:45:30"
/> />
</label> </label>
<label className="race-form__field"> <label className="race-form__field">
<span className="race-form__label">Место на финише</span> <span className="race-form__label">Место на финише</span>
<input <input
className="race-form__input" className="race-form__input"
type="text" type="text"
name="finishPlace" name="finishPlace"
value={form.finishPlace} value={form.finishPlace}
onChange={handleChange} onChange={handleChange}
placeholder="12/340" placeholder="12/340"
/> />
</label> </label>
</fieldset> </fieldset>
) : null}
<fieldset className="race-form__group"> <fieldset className="race-form__group">
<legend className="race-form__legend">Дополнительно</legend> <legend className="race-form__legend">Дополнительно</legend>

View File

@@ -10,7 +10,8 @@ import {
getRaceStatusClassName, getRaceStatusClassName,
getRaceStatusLabel, getRaceStatusLabel,
parseRaceDate, parseRaceDate,
splitRacesByDate, sortByDateAsc,
sortByDateDesc,
} from "../lib"; } from "../lib";
const MONTH_OPTIONS: { value: string; label: string }[] = [ const MONTH_OPTIONS: { value: string; label: string }[] = [
@@ -220,7 +221,32 @@ export function RacesPage(): JSX.Element {
}; };
}, [listQuery]); }, [listQuery]);
const { upcoming, past } = useMemo(() => splitRacesByDate(races), [races]); const { upcoming, completed } = useMemo(
() => ({
upcoming: sortByDateAsc(races.filter((race) => race.status !== "completed")),
completed: sortByDateDesc(races.filter((race) => race.status === "completed")),
}),
[races],
);
const statusMessage = useMemo(() => {
if (errorMessage && !isLoading) {
return errorMessage;
}
if (isLoading) {
return "Загружаем данные...";
}
if (viewMode === "calendar" && monthFilter === "") {
return "Выберите месяц, чтобы увидеть его крупным планом.";
}
return "";
}, [errorMessage, isLoading, monthFilter, viewMode]);
const statusClassName = [
"races-status__message",
!statusMessage ? "races-status__message--empty" : "",
errorMessage && !isLoading ? "races-status__message--error" : "",
]
.filter(Boolean)
.join(" ");
if (errorMessage && races.length === 0 && !isLoading) { if (errorMessage && races.length === 0 && !isLoading) {
return ( return (
@@ -294,26 +320,21 @@ export function RacesPage(): JSX.Element {
</div> </div>
</section> </section>
{errorMessage && !isLoading ? ( <div className="races-status" aria-live="polite">
<p className="page__subtitle page__subtitle--error" role="alert" style={{ marginTop: "var(--space-4)" }}> <p
{errorMessage} className={statusClassName}
role={errorMessage && !isLoading ? "alert" : undefined}
aria-busy={isLoading || undefined}
aria-hidden={!statusMessage || undefined}
>
{statusMessage || "\u00a0"}
</p> </p>
) : null} </div>
{viewMode === "calendar" && monthFilter === "" ? (
<p className="page__subtitle races-cal__filter-hint">Выберите месяц, чтобы увидеть его крупным планом.</p>
) : null}
{isLoading ? (
<p className="page__subtitle" aria-busy="true">
Загружаем данные...
</p>
) : null}
{viewMode === "list" ? ( {viewMode === "list" ? (
<div className="race-lists"> <div className="race-lists">
<RaceList title="Будущие" races={upcoming} /> <RaceList title="Будущие" races={upcoming} />
<RaceList title="Прошедшие" races={past} /> <RaceList title="Завершенные" races={completed} />
</div> </div>
) : ( ) : (
<div className="races-cal-wrap"> <div className="races-cal-wrap">

View File

@@ -45,6 +45,12 @@ a {
font-weight: 700; font-weight: 700;
} }
.app-shell__brand:hover,
.app-shell__brand:focus-visible {
color: var(--color-text);
outline: none;
}
.app-shell__nav { .app-shell__nav {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -961,6 +967,25 @@ a {
margin-top: var(--space-6); margin-top: var(--space-6);
} }
.races-status {
min-height: calc(var(--line-height-base) * var(--font-size-caption));
margin-top: var(--space-4);
}
.races-status__message {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-caption);
}
.races-status__message--empty {
visibility: hidden;
}
.races-status__message--error {
color: var(--color-danger);
}
.races-cal__filter-hint { .races-cal__filter-hint {
margin-top: var(--space-3); margin-top: var(--space-3);
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -1006,6 +1031,26 @@ a {
color: var(--color-text); color: var(--color-text);
} }
.races-cal__month-title-button {
display: inline-flex;
align-items: center;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: inherit;
cursor: pointer;
}
.races-cal__month-title-button:hover,
.races-cal__month-title-button:focus-visible {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
outline: none;
}
.races-cal__weekdays { .races-cal__weekdays {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
@@ -1040,6 +1085,12 @@ a {
color: var(--color-accent); color: var(--color-accent);
} }
.races-cal__cell--past .races-cal__day-btn {
color: var(--color-text-muted);
background: #f0f2f4;
border-color: #d8dde2;
}
.races-cal__cell--open { .races-cal__cell--open {
z-index: 2; z-index: 2;
} }
@@ -1070,6 +1121,12 @@ a {
border-color: var(--color-accent); border-color: var(--color-accent);
} }
.races-cal__cell--today .races-cal__day-btn,
.races-cal__cell--today .races-cal__day-btn:hover,
.races-cal__cell--today .races-cal__day-btn:focus-visible {
border-color: var(--color-text);
}
.races-cal__popover { .races-cal__popover {
position: absolute; position: absolute;
top: calc(100% + var(--space-1)); top: calc(100% + var(--space-1));
@@ -1226,6 +1283,11 @@ body {
color: var(--color-bg-deep); color: var(--color-bg-deep);
} }
.app-shell__brand:hover,
.app-shell__brand:focus-visible {
color: var(--color-accent);
}
.app-shell__link { .app-shell__link {
font-size: var(--font-size-caption); font-size: var(--font-size-caption);
font-weight: 700; font-weight: 700;
@@ -1290,7 +1352,7 @@ body {
color: #ffffff; color: #ffffff;
background: background:
linear-gradient(90deg, rgba(7, 25, 39, 0.9) 0%, rgba(7, 25, 39, 0.58) 46%, rgba(7, 25, 39, 0.1) 100%), linear-gradient(90deg, rgba(7, 25, 39, 0.9) 0%, rgba(7, 25, 39, 0.58) 46%, rgba(7, 25, 39, 0.1) 100%),
url("/images/runner-hero.png") center / cover; url("/images/runner-hero.jpg") center / cover;
} }
.dashboard-hero__content, .dashboard-hero__content,
@@ -1525,7 +1587,7 @@ body {
padding: var(--space-8); padding: var(--space-8);
background: background:
linear-gradient(90deg, rgba(7, 25, 39, 0.88), rgba(7, 25, 39, 0.48), rgba(7, 25, 39, 0.08)), linear-gradient(90deg, rgba(7, 25, 39, 0.88), rgba(7, 25, 39, 0.48), rgba(7, 25, 39, 0.08)),
url("/images/race-half.png") center / cover; url("/images/race-half.jpg") center / cover;
} }
.races-hero .page__title, .races-hero .page__title,
@@ -1747,18 +1809,61 @@ body {
font-weight: 800; font-weight: 800;
} }
.races-cal__month-title-button {
border-radius: var(--radius-sm);
transition:
color 0.15s ease,
text-decoration-color 0.15s ease;
}
.races-cal__month-title-button:hover,
.races-cal__month-title-button:focus-visible {
color: var(--color-accent);
}
.races-cal__cell--has-race .races-cal__day-btn { .races-cal__cell--has-race .races-cal__day-btn {
color: #071927; color: #071927;
background: linear-gradient(135deg, rgba(185, 242, 74, 0.7), rgba(17, 104, 216, 0.14)); background: linear-gradient(135deg, rgba(185, 242, 74, 0.7), rgba(17, 104, 216, 0.14));
border-color: rgba(17, 104, 216, 0.18); border-color: rgba(17, 104, 216, 0.18);
} }
.races-cal__cell--past .races-cal__day-btn,
.races-cal__cell--past.races-cal__cell--has-race .races-cal__day-btn {
color: #6f7c87;
background: #eef1f3;
border-color: #d4dbe1;
}
.races-cal__cell--has-race .races-cal__day-btn:hover, .races-cal__cell--has-race .races-cal__day-btn:hover,
.races-cal__cell--has-race .races-cal__day-btn:focus-visible { .races-cal__cell--has-race .races-cal__day-btn:focus-visible {
background: var(--color-lime); background: var(--color-lime);
border-color: var(--color-bg-deep); border-color: var(--color-bg-deep);
} }
.races-cal__cell--past .races-cal__day-btn:hover,
.races-cal__cell--past .races-cal__day-btn:focus-visible,
.races-cal__cell--past.races-cal__cell--has-race .races-cal__day-btn:hover,
.races-cal__cell--past.races-cal__cell--has-race .races-cal__day-btn:focus-visible {
color: #52616d;
background: #e3e8ec;
border-color: #aeb9c3;
}
.races-cal__cell--today .races-cal__day-btn,
.races-cal__cell--today.races-cal__cell--has-race .races-cal__day-btn,
.races-cal__cell--today.races-cal__cell--past .races-cal__day-btn {
border-color: #071927;
box-shadow: inset 0 0 0 1px #071927;
}
.races-cal__cell--today .races-cal__day-btn:hover,
.races-cal__cell--today .races-cal__day-btn:focus-visible,
.races-cal__cell--today.races-cal__cell--has-race .races-cal__day-btn:hover,
.races-cal__cell--today.races-cal__cell--has-race .races-cal__day-btn:focus-visible {
border-color: #071927;
box-shadow: inset 0 0 0 1px #071927;
}
.races-cal__popover { .races-cal__popover {
border-color: rgba(17, 104, 216, 0.2); border-color: rgba(17, 104, 216, 0.2);
border-radius: var(--radius-md); border-radius: var(--radius-md);
@@ -1788,7 +1893,7 @@ body {
padding: var(--space-8); padding: var(--space-8);
background: background:
linear-gradient(90deg, rgba(7, 25, 39, 0.9), rgba(7, 25, 39, 0.5), rgba(7, 25, 39, 0.08)), linear-gradient(90deg, rgba(7, 25, 39, 0.9), rgba(7, 25, 39, 0.5), rgba(7, 25, 39, 0.08)),
url("/images/race-short.png") center / cover; url("/images/race-short.jpg") center / cover;
} }
.race-day-hero .page-link { .race-day-hero .page-link {