diff --git a/public/assets/papers/maia1.jpg b/public/assets/papers/maia1.jpg
index 526cf988..28dbcff3 100644
Binary files a/public/assets/papers/maia1.jpg and b/public/assets/papers/maia1.jpg differ
diff --git a/public/assets/papers/maia2.jpg b/public/assets/papers/maia2.jpg
index 99ceb163..2c63d50b 100644
Binary files a/public/assets/papers/maia2.jpg and b/public/assets/papers/maia2.jpg differ
diff --git a/public/assets/papers/maia3.png b/public/assets/papers/maia3.png
index 3115be0a..ebdb75b5 100644
Binary files a/public/assets/papers/maia3.png and b/public/assets/papers/maia3.png differ
diff --git a/public/assets/team/arthur.png b/public/assets/team/arthur.png
index 97abc865..63121fbc 100644
Binary files a/public/assets/team/arthur.png and b/public/assets/team/arthur.png differ
diff --git a/public/assets/team/ashton.jpeg b/public/assets/team/ashton.jpeg
index 7e9b01e2..411401f5 100644
Binary files a/public/assets/team/ashton.jpeg and b/public/assets/team/ashton.jpeg differ
diff --git a/public/assets/team/daniel.png b/public/assets/team/daniel.png
index ff3e0d08..c179ba15 100644
Binary files a/public/assets/team/daniel.png and b/public/assets/team/daniel.png differ
diff --git a/public/assets/team/dmitriy.jpg b/public/assets/team/dmitriy.jpg
index de0b7d31..69605f04 100644
Binary files a/public/assets/team/dmitriy.jpg and b/public/assets/team/dmitriy.jpg differ
diff --git a/public/assets/team/george.png b/public/assets/team/george.png
index b6711160..1ee4d98f 100644
Binary files a/public/assets/team/george.png and b/public/assets/team/george.png differ
diff --git a/public/assets/team/isaac.jpg b/public/assets/team/isaac.jpg
index 5928029f..906ef5f8 100644
Binary files a/public/assets/team/isaac.jpg and b/public/assets/team/isaac.jpg differ
diff --git a/public/assets/team/jon.jpg b/public/assets/team/jon.jpg
index 4604dc11..9b33d8d2 100644
Binary files a/public/assets/team/jon.jpg and b/public/assets/team/jon.jpg differ
diff --git a/public/assets/team/joseph.jpg b/public/assets/team/joseph.jpg
index e7c9d2c6..9ff91069 100644
Binary files a/public/assets/team/joseph.jpg and b/public/assets/team/joseph.jpg differ
diff --git a/public/assets/team/kevin.jpg b/public/assets/team/kevin.jpg
index 918c5a7a..ef1101c8 100644
Binary files a/public/assets/team/kevin.jpg and b/public/assets/team/kevin.jpg differ
diff --git a/public/assets/team/reid.jpg b/public/assets/team/reid.jpg
index e038cbe8..588a8834 100644
Binary files a/public/assets/team/reid.jpg and b/public/assets/team/reid.jpg differ
diff --git a/public/assets/team/sid.jpeg b/public/assets/team/sid.jpeg
index 80c2524b..5fd26850 100644
Binary files a/public/assets/team/sid.jpeg and b/public/assets/team/sid.jpeg differ
diff --git a/public/favicon.png b/public/favicon.png
index 5eafecf7..c50047dd 100644
Binary files a/public/favicon.png and b/public/favicon.png differ
diff --git a/scripts/optimize-images.mjs b/scripts/optimize-images.mjs
new file mode 100644
index 00000000..c546c566
--- /dev/null
+++ b/scripts/optimize-images.mjs
@@ -0,0 +1,83 @@
+#!/usr/bin/env node
+/**
+ * Resize oversized homepage source images in place.
+ *
+ * These assets (team avatars, paper-card previews) are only rendered on the
+ * homepage at small sizes via next/image, but the source files ship at up to
+ * 2673px / 1.6MB. We downscale them to ~2x their largest display size so the
+ * repo stays lean and the Next image optimizer has less work to do. Final
+ * delivery format/size is still handled by next/image at request time.
+ *
+ * Usage: node scripts/optimize-images.mjs [--dry]
+ */
+import { readdir, stat } from 'node:fs/promises'
+import path from 'node:path'
+import sharp from 'sharp'
+
+const DRY = process.argv.includes('--dry')
+const ROOT = path.resolve(import.meta.dirname, '..')
+
+// dir -> max edge (px). 2x the largest rendered size.
+const TARGETS = [
+ { dir: 'public/assets/team', maxEdge: 320 },
+ { dir: 'public/assets/papers', maxEdge: 900 },
+]
+
+const EXEC = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png' }
+
+const kb = (n) => `${(n / 1024).toFixed(0)}KB`
+
+let beforeTotal = 0
+let afterTotal = 0
+
+for (const { dir, maxEdge } of TARGETS) {
+ const abs = path.join(ROOT, dir)
+ let files
+ try {
+ files = await readdir(abs)
+ } catch {
+ console.warn(`skip missing dir ${dir}`)
+ continue
+ }
+ for (const file of files) {
+ const ext = path.extname(file).toLowerCase()
+ const fmt = EXEC[ext]
+ if (!fmt) continue
+ const fp = path.join(abs, file)
+ const before = (await stat(fp)).size
+ const img = sharp(fp)
+ const meta = await img.metadata()
+ const longest = Math.max(meta.width || 0, meta.height || 0)
+
+ let pipeline = sharp(fp).rotate() // respect EXIF orientation
+ if (longest > maxEdge) {
+ pipeline = pipeline.resize({
+ width: meta.width >= meta.height ? maxEdge : null,
+ height: meta.height > meta.width ? maxEdge : null,
+ withoutEnlargement: true,
+ })
+ }
+ pipeline =
+ fmt === 'jpeg'
+ ? pipeline.jpeg({ quality: 82, mozjpeg: true })
+ : pipeline.png({ compressionLevel: 9, palette: true })
+
+ const buf = await pipeline.toBuffer()
+ beforeTotal += before
+ // Only rewrite if we actually save bytes.
+ if (buf.length < before) {
+ afterTotal += buf.length
+ if (!DRY) await sharp(buf).toFile(fp)
+ console.log(
+ `${file.padEnd(16)} ${String(longest).padStart(4)}px ${kb(before).padStart(7)} -> ${kb(buf.length).padStart(7)} ${DRY ? '(dry)' : ''}`,
+ )
+ } else {
+ afterTotal += before
+ console.log(`${file.padEnd(16)} already optimal (${kb(before)})`)
+ }
+ }
+}
+
+console.log(
+ `\nTotal: ${kb(beforeTotal)} -> ${kb(afterTotal)} (${(((beforeTotal - afterTotal) / beforeTotal) * 100).toFixed(0)}% smaller)`,
+)
diff --git a/src/components/Common/ErrorBoundary.tsx b/src/components/Common/ErrorBoundary.tsx
index 2d83ccf3..bbc45909 100644
--- a/src/components/Common/ErrorBoundary.tsx
+++ b/src/components/Common/ErrorBoundary.tsx
@@ -1,11 +1,16 @@
import Link from 'next/link'
import Head from 'next/head'
+import dynamic from 'next/dynamic'
import { Component } from 'react'
-import Chessground from '@react-chess/chessground'
import { Header } from './Header'
import { Footer } from './Footer'
import { trackErrorEncountered } from 'src/lib/analytics'
+const ErrorChessground = dynamic(() => import('@react-chess/chessground'), {
+ ssr: false,
+ loading: () =>
,
+})
+
interface Props {
children: React.ReactNode
}
@@ -109,7 +114,7 @@ export class ErrorBoundary extends Component {
-
{
const { user } = useContext(AuthContext)
diff --git a/src/components/Common/PlaySetupModal.tsx b/src/components/Common/PlaySetupModal.tsx
index 0d394594..c209f0f9 100644
--- a/src/components/Common/PlaySetupModal.tsx
+++ b/src/components/Common/PlaySetupModal.tsx
@@ -12,7 +12,7 @@ import {
TimeControlOptionNames,
TimeControlOptions,
} from 'src/types'
-import { ModalContext } from 'src/contexts'
+import { ModalContext } from 'src/contexts/ModalContext'
import { ModalContainer } from './ModalContainer'
const maiaOptions = [
@@ -72,7 +72,7 @@ function OptionSelect({
)
}
-interface Props {
+export interface PlaySetupModalProps {
playType: PlayType
player?: Color
timeControl?: TimeControl
@@ -89,7 +89,9 @@ interface Props {
modalSubtitle?: string
}
-export const PlaySetupModal: React.FC = (props: Props) => {
+export const PlaySetupModal: React.FC = (
+ props: PlaySetupModalProps,
+) => {
const { setPlaySetupModalProps } = useContext(ModalContext)
const router = useRouter()
const { push } = router
diff --git a/src/components/Home/AboutMaia.tsx b/src/components/Home/AboutMaia.tsx
index b0d8c400..14d24bbf 100644
--- a/src/components/Home/AboutMaia.tsx
+++ b/src/components/Home/AboutMaia.tsx
@@ -1,3 +1,4 @@
+import Image from 'next/image'
import { motion } from 'framer-motion'
import { TeamMember } from './TeamMember'
import { useInView } from 'react-intersection-observer'
@@ -177,11 +178,13 @@ const PaperCard = ({
{featured && (
-
-
![]()
+
)}
diff --git a/src/components/Home/GameCarousel.tsx b/src/components/Home/GameCarousel.tsx
index 09968885..e115b688 100644
--- a/src/components/Home/GameCarousel.tsx
+++ b/src/components/Home/GameCarousel.tsx
@@ -181,7 +181,10 @@ export const GameCarousel: React.FC = () => {
const router = useRouter()
const [games, setGames] = useState
(SAMPLE_GAMES)
const [isPaused, setIsPaused] = useState(false)
+ const [isCarouselVisible, setIsCarouselVisible] = useState(false)
+ const [shouldLoadLiveGames, setShouldLoadLiveGames] = useState(false)
const abortController = useRef(null)
+ const sectionRef = useRef(null)
const carouselRef = useRef(null)
const handleGameStart = useCallback((gameData: StreamedGame) => {
@@ -300,7 +303,7 @@ export const GameCarousel: React.FC = () => {
}, [])
useEffect(() => {
- if (isPaused || !carouselRef.current) return
+ if (isPaused || !isCarouselVisible || !carouselRef.current) return
const scroll = () => {
if (carouselRef.current) {
@@ -317,18 +320,74 @@ export const GameCarousel: React.FC = () => {
const interval = setInterval(scroll, 20)
return () => clearInterval(interval)
- }, [isPaused])
+ }, [isPaused, isCarouselVisible])
useEffect(() => {
- fetchNewGame()
- fetchBroadcast()
+ const section = sectionRef.current
+
+ if (!section || typeof IntersectionObserver === 'undefined') {
+ setIsCarouselVisible(true)
+ setShouldLoadLiveGames(true)
+ return
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setIsCarouselVisible(entry.isIntersecting)
+
+ if (entry.isIntersecting) {
+ setShouldLoadLiveGames(true)
+ }
+ },
+ { rootMargin: '300px 0px' },
+ )
+
+ observer.observe(section)
+
+ return () => observer.disconnect()
+ }, [])
+
+ useEffect(() => {
+ if (!shouldLoadLiveGames) return
+
+ const startLiveFeeds = () => {
+ fetchNewGame()
+ fetchBroadcast()
+ }
+
+ const windowWithIdleCallback = window as Window & {
+ requestIdleCallback?: (
+ callback: IdleRequestCallback,
+ options?: IdleRequestOptions,
+ ) => number
+ cancelIdleCallback?: (handle: number) => void
+ }
+
+ if (windowWithIdleCallback.requestIdleCallback) {
+ const idleHandle = windowWithIdleCallback.requestIdleCallback(
+ startLiveFeeds,
+ {
+ timeout: 2500,
+ },
+ )
+
+ return () => {
+ windowWithIdleCallback.cancelIdleCallback?.(idleHandle)
+ if (abortController.current) {
+ abortController.current.abort()
+ }
+ }
+ }
+
+ const timeoutId = window.setTimeout(startLiveFeeds, 1000)
return () => {
+ window.clearTimeout(timeoutId)
if (abortController.current) {
abortController.current.abort()
}
}
- }, [fetchNewGame, fetchBroadcast])
+ }, [fetchNewGame, fetchBroadcast, shouldLoadLiveGames])
const handleGameClick = useCallback(
(game: GameData) => {
@@ -340,7 +399,10 @@ export const GameCarousel: React.FC = () => {
)
return (
-
+
= ({ scrollHandler }: Props) => {
// Fetch global stats and set up periodic updates
useEffect(() => {
const fetchGlobalStats = async () => {
- const data = await getGlobalStats()
- setGlobalStats(data)
+ try {
+ const data = await getGlobalStats()
+ setGlobalStats(data)
+ } catch (error) {
+ console.error('Error fetching global stats:', error)
+ }
+ }
+
+ const windowWithIdleCallback = window as Window & {
+ requestIdleCallback?: (
+ callback: IdleRequestCallback,
+ options?: IdleRequestOptions,
+ ) => number
+ cancelIdleCallback?: (handle: number) => void
}
- // Fetch immediately
- fetchGlobalStats()
+ const idleHandle = windowWithIdleCallback.requestIdleCallback?.(
+ fetchGlobalStats,
+ {
+ timeout: 2000,
+ },
+ )
+ const timeoutId =
+ idleHandle === undefined
+ ? window.setTimeout(fetchGlobalStats, 800)
+ : undefined
// Update every 5 minutes
const interval = setInterval(fetchGlobalStats, 5 * 60 * 1000)
- return () => clearInterval(interval)
+ return () => {
+ if (idleHandle !== undefined) {
+ windowWithIdleCallback.cancelIdleCallback?.(idleHandle)
+ }
+ if (timeoutId !== undefined) {
+ window.clearTimeout(timeoutId)
+ }
+ clearInterval(interval)
+ }
}, [])
return (
diff --git a/src/components/Home/Sections/AdditionalFeaturesSection.tsx b/src/components/Home/Sections/AdditionalFeaturesSection.tsx
index 4c0b6526..0aa4e532 100644
--- a/src/components/Home/Sections/AdditionalFeaturesSection.tsx
+++ b/src/components/Home/Sections/AdditionalFeaturesSection.tsx
@@ -3,7 +3,7 @@ import { useContext, ReactNode } from 'react'
import { motion } from 'framer-motion'
import { useInView } from 'react-intersection-observer'
-import { ModalContext } from 'src/contexts'
+import { ModalContext } from 'src/contexts/ModalContext'
import { PlayType } from 'src/types'
import { StarIcon, BrainIcon, BotOrNotIcon } from 'src/components/Common/Icons'
diff --git a/src/components/Home/Sections/PlaySection.tsx b/src/components/Home/Sections/PlaySection.tsx
index cb751c57..1f769d04 100644
--- a/src/components/Home/Sections/PlaySection.tsx
+++ b/src/components/Home/Sections/PlaySection.tsx
@@ -6,7 +6,8 @@ import type { DrawShape } from 'chessground/draw'
import Chessground from '@react-chess/chessground'
import { useInView } from 'react-intersection-observer'
import { useContext, useState, useEffect } from 'react'
-import { ModalContext, AuthContext } from 'src/contexts'
+import { AuthContext } from 'src/contexts/AuthContext'
+import { ModalContext } from 'src/contexts/ModalContext'
const DEMO_POSITION = {
fen: 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R b KQkq - 0 1',
diff --git a/src/components/Home/TeamMember.tsx b/src/components/Home/TeamMember.tsx
index 1ec948f8..30d52b0c 100644
--- a/src/components/Home/TeamMember.tsx
+++ b/src/components/Home/TeamMember.tsx
@@ -1,3 +1,4 @@
+import Image from 'next/image'
import { motion } from 'framer-motion'
import { GithubIcon } from 'src/components/Common/Icons'
interface TeamMemberProps {
@@ -31,9 +32,12 @@ export const TeamMember = ({
ease: 'easeOut',
}}
>
-
diff --git a/src/contexts/ModalContext.tsx b/src/contexts/ModalContext.tsx
index 4e910297..856c7b62 100644
--- a/src/contexts/ModalContext.tsx
+++ b/src/contexts/ModalContext.tsx
@@ -1,13 +1,22 @@
-import { PlaySetupModal } from 'src/components'
-import React, { ComponentProps, ReactNode, useState } from 'react'
+import dynamic from 'next/dynamic'
+import React, { ReactNode, useState } from 'react'
+import type { PlaySetupModalProps } from 'src/components/Common/PlaySetupModal'
+
+const PlaySetupModal = dynamic(
+ () =>
+ import('src/components/Common/PlaySetupModal').then(
+ (mod) => mod.PlaySetupModal,
+ ),
+ { ssr: false },
+)
const fn = () => {
throw new Error('poorly provided ModalContext')
}
interface IModalContext {
- playSetupModalProps?: ComponentProps
- setPlaySetupModalProps: (arg0?: ComponentProps) => void
+ playSetupModalProps?: PlaySetupModalProps
+ setPlaySetupModalProps: (arg0?: PlaySetupModalProps) => void
}
export const ModalContext = React.createContext({
@@ -20,7 +29,7 @@ export const ModalContextProvider: React.FC<{ children: ReactNode }> = ({
children: ReactNode
}) => {
const [playSetupModalProps, setPlaySetupModalProps] = useState<
- ComponentProps | undefined
+ PlaySetupModalProps | undefined
>(undefined)
return (
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 6cd2139e..12cae8d1 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -9,33 +9,35 @@ import { Analytics } from '@vercel/analytics/react'
import { PostHogProvider } from 'posthog-js/react'
import { SoundProvider } from 'src/contexts/SoundContext'
-import {
- AuthContextProvider,
- ModalContextProvider,
- WindowSizeContextProvider,
- AnalysisListContextProvider,
- MaiaEngineContextProvider,
- StockfishEngineContextProvider,
- SettingsProvider,
- TourProvider as TourContextProvider,
-} from 'src/contexts'
+import { AnalysisListContextProvider } from 'src/contexts/AnalysisListContext'
+import { AuthContextProvider } from 'src/contexts/AuthContext'
+import { MaiaEngineContextProvider } from 'src/contexts/MaiaEngineContext'
+import { ModalContextProvider } from 'src/contexts/ModalContext'
+import { SettingsProvider } from 'src/contexts/SettingsContext'
+import { StockfishEngineContextProvider } from 'src/contexts/StockfishEngineContext'
+import { TourProvider as TourContextProvider } from 'src/contexts/TourContext'
+import { WindowSizeContextProvider } from 'src/contexts/WindowSizeContext'
import 'src/styles/tailwind.css'
import 'src/styles/themes.css'
import 'react-tooltip/dist/react-tooltip.css'
import 'node_modules/chessground/assets/chessground.base.css'
import 'node_modules/chessground/assets/chessground.brown.css'
import 'node_modules/chessground/assets/chessground.cburnett.css'
-import {
- Footer,
- Compose,
- ErrorBoundary,
- Header,
- FeedbackButton,
-} from 'src/components'
+import { Compose } from 'src/components/Common/Compose'
+import { ErrorBoundary } from 'src/components/Common/ErrorBoundary'
+import { FeedbackButton } from 'src/components/Common/FeedbackButton'
+import { Footer } from 'src/components/Common/Footer'
+import { Header } from 'src/components/Common/Header'
import { browserPostHogConfig } from 'src/lib/posthog-browser-config'
const openSansClassName = 'font-sans'
const OG_IMAGE_URL = 'https://www.maiachess.com/assets/og-maia.png'
+// Only wght 200-400 and FILL 0/1 are actually used, so pin opsz=24/GRAD=0 and
+// narrow the ranges: Google serves a ~1.1MB instance instead of the full
+// ~3.8MB variable font. The material-symbols-ready guard hides icons until the
+// font loads, so display=swap never flashes the ligature text.
+const MATERIAL_SYMBOLS_STYLESHEET =
+ 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,200..400,0..1,0&display=swap'
function MaiaPlatform({ Component, pageProps }: AppProps) {
const router = useRouter()
@@ -64,6 +66,28 @@ function MaiaPlatform({ Component, pageProps }: AppProps) {
})
}, [])
+ useEffect(() => {
+ const root = document.documentElement
+ const markMaterialSymbolsReady = () => {
+ root.classList.add('material-symbols-ready')
+ }
+
+ if (!('fonts' in document)) {
+ markMaterialSymbolsReady()
+ return
+ }
+
+ if (document.fonts.check('24px "Material Symbols Outlined"')) {
+ markMaterialSymbolsReady()
+ return
+ }
+
+ document.fonts
+ .load('24px "Material Symbols Outlined"')
+ .then(markMaterialSymbolsReady)
+ .catch(() => undefined)
+ }, [])
+
return (
@@ -82,9 +106,22 @@ function MaiaPlatform({ Component, pageProps }: AppProps) {
>
+
+
+
+
+
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 8527c5d3..4e0f45e9 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,19 +1,66 @@
import Head from 'next/head'
+import dynamic from 'next/dynamic'
import type { NextPage } from 'next'
import React, { useCallback, useContext, useEffect, useRef } from 'react'
-import { ModalContext } from 'src/contexts'
-import {
- HomeHero,
- AboutMaia,
- PlaySection,
- AnalysisSection,
- TrainSection,
- AdditionalFeaturesSection,
- PageNavigation,
-} from 'src/components'
+import { ModalContext } from 'src/contexts/ModalContext'
+import { AboutMaia } from 'src/components/Home/AboutMaia'
+import { HomeHero } from 'src/components/Home/HomeHero'
+import { PageNavigation } from 'src/components/Home/PageNavigation'
import { GameCarousel } from 'src/components/Home/GameCarousel'
+const HomeSectionFallback = ({ id }: { id: string }) => (
+
+)
+
+const PlaySection = dynamic(
+ () =>
+ import('src/components/Home/Sections/PlaySection').then(
+ (mod) => mod.PlaySection,
+ ),
+ {
+ ssr: false,
+ loading: () => ,
+ },
+)
+
+const AnalysisSection = dynamic(
+ () =>
+ import('src/components/Home/Sections/AnalysisSection').then(
+ (mod) => mod.AnalysisSection,
+ ),
+ {
+ ssr: false,
+ loading: () => ,
+ },
+)
+
+const TrainSection = dynamic(
+ () =>
+ import('src/components/Home/Sections/TrainSection').then(
+ (mod) => mod.TrainSection,
+ ),
+ {
+ ssr: false,
+ loading: () => ,
+ },
+)
+
+const AdditionalFeaturesSection = dynamic(
+ () =>
+ import('src/components/Home/Sections/AdditionalFeaturesSection').then(
+ (mod) => mod.AdditionalFeaturesSection,
+ ),
+ {
+ ssr: false,
+ loading: () => ,
+ },
+)
+
const Home: NextPage = () => {
const { setPlaySetupModalProps } = useContext(ModalContext)
diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css
index 3a9902b0..609bbe82 100644
--- a/src/styles/tailwind.css
+++ b/src/styles/tailwind.css
@@ -35,6 +35,26 @@ h5 {
margin: 0;
}
+.material-symbols-outlined {
+ font-family: 'Material Symbols Outlined';
+ font-weight: normal;
+ font-style: normal;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ font-feature-settings: 'liga';
+ -webkit-font-feature-settings: 'liga';
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ overflow: hidden;
+}
+
.material-symbols-filled {
font-variation-settings:
'FILL' 1,
@@ -163,7 +183,8 @@ svg {
.red-scrollbar {
scrollbar-width: thin;
- scrollbar-color: rgb(var(--color-human-accent3)) rgb(var(--color-backdrop) / 0.22);
+ scrollbar-color: rgb(var(--color-human-accent3))
+ rgb(var(--color-backdrop) / 0.22);
}
.red-scrollbar::-webkit-scrollbar {