diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d06806d..2b9b890 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,64 +1,53 @@ import { ErrorBoundary } from './components/ErrorBoundary' import Matches from './pages/Matches' -function Logo() { - // 纯 SVG 队徽/足球图形,替代 emoji ⚽(跨平台渲染不一致) - return ( - - - - ) +/** 报眉日期行:2026年9月15日 星期二 */ +function dateLine(): string { + return new Date().toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'long', + }) } export default function App() { return ( -
-
-
-
- -
-

- 先知 Profeto -

-

- LLM 多专家协作 · 足球比分预测 -

-
+
+ {/* ── 报头:粗线 + 居中刊名 + 报眉 ── */} +
+
+
+

+ 先知 + + Profeto + +

+

+ 足球比分预测 · 五路专家 · 终裁汇总 +

- - - - +
+ {dateLine()} + + - 服务运行中 - +
-
+
-
-

- 预测结果由大语言模型生成,仅供研究参考,不构成任何投注建议 -

+ {/* ── 版底 ── */} +
+
+ 预测结果由大语言模型生成 · 仅供研究参考 · 不构成任何投注建议 +
diff --git a/frontend/src/index.css b/frontend/src/index.css index 9845f6d..eac7833 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -9,13 +9,18 @@ } body { - @apply bg-ink-100 text-ink-800 antialiased; + @apply bg-paper-50 text-ink-800 font-sans antialiased; font-feature-settings: 'tnum' 1; /* 数字等宽:比分/百分比不跳动 */ } - /* 统一焦点环,键盘可达性 */ + ::selection { + @apply bg-press-wash text-press; + } + + /* 统一焦点环:印报红细线,键盘可达 */ :focus-visible { - @apply outline-none ring-2 ring-brand-500/40 ring-offset-1 ring-offset-white; + outline: 2px solid #9e1b1b; + outline-offset: 2px; } /* 细滚动条 */ @@ -27,7 +32,7 @@ background: transparent; } ::-webkit-scrollbar-thumb { - @apply rounded-pill bg-ink-300/70; + @apply rounded-full bg-ink-300; } ::-webkit-scrollbar-thumb:hover { @apply bg-ink-400; @@ -46,53 +51,52 @@ } @layer components { - /* ── 卡片 ── */ - .card { - @apply rounded-card border border-ink-200 bg-white shadow-card; + /* ── 报头双线:粗线在上、细线在下 ── */ + .masthead-rule { + border-top: 3px solid #17140f; } - /* ── 按钮 ── */ + /* ── 按钮:方正边框式,悬停反白 ── */ .btn { - @apply inline-flex items-center justify-center gap-1.5 rounded-lg px-3.5 py-2 - text-sm font-medium transition-colors duration-150 - disabled:cursor-not-allowed disabled:opacity-50; - } - .btn-primary { - @apply btn bg-brand-600 text-white shadow-sm hover:bg-brand-700 active:bg-brand-800; - } - .btn-ghost { - @apply btn border border-ink-200 bg-white text-ink-700 hover:bg-ink-50 hover:border-ink-300; + @apply inline-flex items-center justify-center gap-1.5 border border-ink-300 bg-transparent px-3 py-1.5 + text-sm text-ink-700 transition-colors duration-150 + hover:border-ink-900 hover:bg-ink-900 hover:text-paper-50 + disabled:cursor-not-allowed disabled:opacity-40 + disabled:hover:border-ink-300 disabled:hover:bg-transparent disabled:hover:text-ink-700; } .btn-sm { - @apply px-3 py-1.5 text-xs; + @apply px-2.5 py-1 text-xs; + } + .btn-solid { + @apply border-ink-900 bg-ink-900 text-paper-50 hover:border-press hover:bg-press; } - /* ── 表单控件 ── */ + /* ── 表单控件:方正、无圆角 ── */ .field { - @apply rounded-lg border border-ink-200 bg-white px-3 py-2 text-sm text-ink-800 - transition-colors hover:border-ink-300 - focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20; + @apply border border-ink-300 bg-transparent px-2.5 py-1.5 text-sm text-ink-800 + transition-colors hover:border-ink-400 + focus:border-press focus:outline-none; } - /* ── 状态胶囊 ── */ - .pill { - @apply inline-flex items-center gap-1 rounded-pill px-2 py-0.5 text-2xs font-medium; + /* ── 版面切换文字标签(联赛/状态/模式) ── */ + .tab { + @apply whitespace-nowrap px-0.5 py-1 text-sm text-ink-500 transition-colors hover:text-ink-900; + } + .tab-on { + @apply font-medium text-press; + } + .tab-on::after { + content: ''; + @apply absolute inset-x-0 bottom-0 h-0.5 bg-press; } - /* ── 骨架屏 ── */ + /* ── 小节标题:宋体加粗 + 墨色底线 ── */ + .section-head { + @apply border-b border-ink-900 pb-1.5 font-serif text-sm font-bold text-ink-900; + } + + /* ── 骨架占位:低调脉动,不用渐变扫光 ── */ .skeleton { - @apply rounded bg-gradient-to-r from-ink-200 via-ink-100 to-ink-200 - bg-[length:200%_100%] animate-shimmer; - } - - /* ── 分段控件(模式切换) ── */ - .segment { - @apply rounded-md px-3 py-1.5 text-xs font-medium transition-colors duration-150; - } - .segment-on { - @apply bg-white text-brand-700 shadow-sm; - } - .segment-off { - @apply text-ink-500 hover:text-ink-700; + @apply animate-pulse rounded-none bg-ink-200; } } diff --git a/frontend/src/pages/Matches.tsx b/frontend/src/pages/Matches.tsx index 8940b8f..5b32364 100644 --- a/frontend/src/pages/Matches.tsx +++ b/frontend/src/pages/Matches.tsx @@ -65,68 +65,37 @@ const LEAGUES = [ { code: 'F1', name: '法甲' }, ] +/** 汉字编号,给专家意见排版用 */ +const CN_NUM = ['一', '二', '三', '四', '五', '六', '七', '八'] + const STATUS_META: Record = { - finished: { label: '已完赛', cls: 'bg-ink-100 text-ink-600' }, - scheduled: { label: '未开赛', cls: 'bg-brand-50 text-brand-700' }, - live: { label: '进行中', cls: 'bg-emerald-50 text-emerald-700' }, + finished: { label: '已完赛', cls: 'text-ink-400' }, + scheduled: { label: '未开赛', cls: 'text-ink-600' }, + live: { label: '进行中', cls: 'text-press font-medium' }, } /** 1x2 → 中文标签 */ const OUTCOME_LABEL: Record = { '1': '主胜', X: '平局', '2': '客胜' } -/** 队名取首字作视觉标记(替代队徽图片,避免额外资源与 404) */ -function initial(name: string): string { - return (name || '?').trim().charAt(0) -} - -/** 依据队名生成一个稳定的色相,让不同球队有可区分的淡色底 */ -function hueOf(name: string): number { - let h = 0 - for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360 - return h -} - -function TeamMark({ name, size = 40 }: { name: string; size?: number }) { - const h = hueOf(name) - return ( - - ) -} - -/** 置信度条:把 0~1 的数值可视化,比纯数字更易读 */ -function Meter({ value, tone = 'brand' }: { value: number; tone?: 'brand' | 'emerald' | 'amber' }) { +/** 置信度细线:0~1 数值的低调可视化 */ +function Meter({ value }: { value: number }) { const pct = Math.max(0, Math.min(100, Math.round(value * 100))) - const bar = - tone === 'emerald' ? 'bg-emerald-500' : tone === 'amber' ? 'bg-amber-500' : 'bg-brand-500' return ( -
-
+
+
) } -/** home_edge(-1~1,正=利主队)的可视化:以中线为原点的双向条 */ +/** home_edge(-1~1,正=利主队)的可视化:以中线为原点的双向细条 */ function EdgeBar({ value }: { value: number }) { const v = Math.max(-1, Math.min(1, value)) const half = Math.abs(v) * 50 return ( -
- +
+ = 0 ? 'bg-brand-500' : 'bg-amber-500'}`} + className={`absolute top-0 h-px transition-all duration-500 ${v >= 0 ? 'bg-press' : 'bg-ink-600'}`} style={ v >= 0 ? { left: '50%', width: `${half}%` } @@ -137,24 +106,76 @@ function EdgeBar({ value }: { value: number }) { ) } -/** 骨架屏行,替代 emoji spinner */ +/** 骨架占位行:低调脉动灰块 */ function SkeletonRows({ n = 4 }: { n?: number }) { return ( <> {Array.from({ length: n }).map((_, i) => ( -
-
-
-
-
-
-
+
+
+
+
+
+
))} ) } +function Spinner({ className = '' }: { className?: string }) { + return ( + + ) +} + +/** 胜平负一行文字:选中的红字加方块标记,未选中的退灰 */ +function OutcomeLine({ + pick, + confidence, +}: { + pick: string | null + confidence: number | null +}) { + const options = ['1', 'X', '2'] as const + return ( +
+
+ {options.map(o => { + const on = pick === o + return ( +
+ + {on && + {on && confidence !== null && ( + + 置信 {Math.round(confidence * 100)}% + + )} +
+ ) + })} +
+ {pick && confidence !== null && ( +
+ +

主观置信度,非统计概率

+
+ )} +
+ ) +} + export default function Matches() { const [league, setLeague] = useState('E0') const [status, setStatus] = useState('scheduled') @@ -196,8 +217,7 @@ export default function Matches() { } }, [league, status]) - // 加载下一页(游标分页)。后端已支持 cursor,前端此前未使用, - // 导致 limit=50 之后的数据永远看不到。 + // 加载下一页(游标分页) const loadMore = async () => { if (!nextCursor || loadingMore) return const seq = loadSeq.current // 不做自增:切换筛选会自增,这里只跟随当前序列 @@ -256,69 +276,86 @@ export default function Matches() { const leagueName = LEAGUES.find(l => l.code === league)?.name ?? league - return ( -
- {/* ── 工具栏 ── */} -
-
- - -
- -
- - -
- -
- -
- - -
-
- -
- - 共 {matches.length} 场 - - -
+ + ))} + + ) + + return ( +
+ {/* ── 联赛版面切换 ── */} + + + {/* ── 第二行:状态 / 模式 / 计数 / 刷新 ── */} +
+ + 状态 + + + + + 模式 + setMode(v as 'single' | 'multi')} + items={[ + { v: 'single', label: '单次', title: '单次调用,快但只有一个模型看全部数据' }, + { v: 'multi', label: '多专家', title: '5 个专家并行分析后由终裁汇总,质量更高' }, + ]} + /> + + + + 共 {matches.length} 场 + +
{/* ── 错误提示 ── */} {error && ( -
-
- -
-

请求失败

-

{error}

-
+
+
+

请求失败

+

{error}

-
)} - {/* ── 比赛列表 ── */} -
+ {/* ── 赛程栏:表格化,行间细线 ── */} +
{loading && } {!loading && matches.length === 0 && ( -
- -

暂无比赛数据

-

请先通过采集接口导入 {leagueName} 的赛程

+
+

本版暂无赛程

+

请先通过采集接口导入 {leagueName} 的比赛数据

)} {!loading && matches.map(m => { - const st = STATUS_META[m.match_status] ?? { label: m.match_status, cls: 'bg-ink-100 text-ink-600' } + const st = STATUS_META[m.match_status] ?? { label: m.match_status, cls: 'text-ink-400' } const homeName = m.home_team_zh || m.home_team const awayName = m.away_team_zh || m.away_team const busy = predictingId === m.id @@ -351,70 +384,58 @@ export default function Matches() { return (
-
- {/* 对阵 */} -
-
- {homeName} - +
+ {/* 日期 + 状态:移动端同行,桌面端日期单独归列 */} +
+ {fmtDate(m.match_date)} + {st.label} +
+ + {/* 对阵:移动端主队/比分/客队同一行,桌面端 sm:contents 拆回 grid 列 */} +
+ {/* 主队(右对齐) */} +
+ {homeName}
-
+ {/* 比分 / VS */} +
{m.home_goals !== null && m.away_goals !== null ? ( - - {m.home_goals}:{m.away_goals} + + {m.home_goals}:{m.away_goals} ) : ( - VS + VS )} {m.home_xg !== null && m.away_xg !== null && ( - + xG {m.home_xg.toFixed(1)}-{m.away_xg.toFixed(1)} )}
-
- - {awayName} + {/* 客队(左对齐) */} +
+ {awayName}
-
- {/* 元信息:桌面端独立成列,移动端与按钮同排 */} -
- - - {st.label} - - - {m.league_code ?? '—'} · {fmtDate(m.match_date)} - -
+ {/* 状态列(桌面) */} + {st.label} - {/* 操作:主功能提升为实心按钮 */} + {/* 预测按钮 */} +
@@ -423,96 +444,39 @@ export default function Matches() { })} {!loading && nextCursor && ( -
-
)} -
+
- {/* ── 预测中:占位卡片(替代 emoji 提示条) ── */} + {/* ── 预测中占位 ── */} {predictingId && !prediction && ( -
-
- - 正在生成预测 - - {mode === 'multi' ? '· 5 个专家并行分析后由终裁汇总,约需 20-60 秒' : '· 单次调用,约需 5-15 秒'} +
+
+ + 正在生成预测 + + {mode === 'multi' ? '五路专家并行分析后终裁,约需 20-60 秒' : '单次调用,约需 5-15 秒'}
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
)} - {/* ── 预测结果 ── */} + {/* ── 预测版 ── */} {prediction && predictionFor && ( - - )} -
- ) -} - -function Spinner({ className = '' }: { className?: string }) { - // 纯 SVG 转圈,替代 emoji ⏳(各平台渲染不一致) - return ( - - ) -} - -/** 胜平负概率条:LLM 只给主观置信度,这里把它按 1x2 结果做成单条高亮, - * 并显式标注「主观」避免用户误当概率 */ -function OutcomeCard({ - pick, - confidence, -}: { - pick: string | null - confidence: number | null -}) { - const label = pick ? OUTCOME_LABEL[pick] ?? pick : '—' - const tone = pick === '1' ? 'brand' : pick === 'X' ? 'amber' : 'emerald' - const color = - tone === 'brand' ? 'text-brand-700' : tone === 'amber' ? 'text-amber-700' : 'text-emerald-700' - const bg = tone === 'brand' ? 'bg-brand-50' : tone === 'amber' ? 'bg-amber-50' : 'bg-emerald-50' - const border = - tone === 'brand' ? 'border-brand-100' : tone === 'amber' ? 'border-amber-100' : 'border-emerald-100' - - return ( -
-

胜平负

-

{label}

- {confidence !== null && ( -
- -

主观置信度 {Math.round(confidence * 100)}%

-
+ )}
) @@ -532,129 +496,98 @@ function PredictionPanel({ const okReports = (prediction.agent_outputs ?? []).filter(r => r.status === 'ok') return ( -
- {/* 面板头 */} -
-
- - - -

LLM 预测结果

- {prediction.mode === 'multi' && ( - - 多 Agent · {okReports.length}/{prediction.agent_outputs?.length ?? 0} 专家有效 - - )} -
- +
+ {/* 版头 */} +
+

+ 预测版 · {homeName} 对 {awayName} +

+ {prediction.provider} / {prediction.model} {prediction.latency_ms !== null && ` · ${(prediction.latency_ms / 1000).toFixed(1)}s`}
-
- {/* ── 对阵 + 预测比分(核心) ── */} -
-
- {homeName} - -
- -
-
- - {prediction.pred_home_goals ?? '-'} - - : - - {prediction.pred_away_goals ?? '-'} - -
- 预测比分 -
- -
- - {awayName} -
+
+ {/* ── 预测比分:版面核心,大号宋体 ── */} +
+

+ {prediction.pred_home_goals ?? '-'} + : + {prediction.pred_away_goals ?? '-'} +

+

预测比分

- {/* ── 指标卡 ── */} -
- -
-

预测模式

-

{mode === 'multi' ? '多 Agent' : '单次'}

-

- prompt {prediction.prompt_version ?? '—'} -

-
-
-

模型耗时

-

- {prediction.latency_ms !== null ? (prediction.latency_ms / 1000).toFixed(1) : '—'} - s -

-

含专家并行 + 终裁汇总

-
+ {/* ── 胜平负 ── */} +
+
- {/* ── 专家报告 ── */} + {/* ── 元信息一行 ── */} +

+ {mode === 'multi' ? `多专家模式 · ${okReports.length}/${prediction.agent_outputs?.length ?? 0} 路有效` : '单次模式'} + {prediction.prompt_version && ` · prompt ${prediction.prompt_version}`} + {prediction.latency_ms !== null && ` · 终裁耗时 ${(prediction.latency_ms / 1000).toFixed(1)} 秒`} +

+ + {/* ── 专家意见 ── */} {prediction.agent_outputs && prediction.agent_outputs.length > 0 && ( -
-
-

专家 Agent 报告

+
+
+ 五路专家意见 {prediction.agent_weights && ( - - 终裁权重: - {Object.entries(prediction.agent_weights) + + 终裁权重:{Object.entries(prediction.agent_weights) .sort((a, b) => b[1] - a[1]) .map(([k, v]) => `${AGENT_LABELS[k] ?? k} ${Math.round(v * 100)}%`) - .join(' · ')} + .join(' / ')} )}
-
- {prediction.agent_outputs.map(r => ( - +
+ {prediction.agent_outputs.map((r, i) => ( + ))}
-
+
)} - {/* ── 推理过程 ── */} + {/* ── 终裁意见:引文式,红竖线 ── */} {prediction.reasoning && ( -
-

终裁推理

-

{prediction.reasoning}

-
+
+

终裁意见

+
+

+ {prediction.reasoning} +

+
+
)} {/* ── 原始上下文 ── */}
- -
+          
             {prediction.context}
           
-
+
) } const STATUS_BADGE: Record = { - ok: { label: '正常', cls: 'bg-emerald-50 text-emerald-700 border-emerald-100' }, - no_data: { label: '无数据', cls: 'bg-ink-100 text-ink-500 border-ink-200' }, - error: { label: '调用失败', cls: 'bg-red-50 text-red-600 border-red-100' }, - parse_error: { label: '解析失败', cls: 'bg-amber-50 text-amber-700 border-amber-100' }, + ok: { label: '正常', cls: 'text-ink-500' }, + no_data: { label: '无数据', cls: 'text-ink-400' }, + error: { label: '调用失败', cls: 'text-press' }, + parse_error: { label: '解析失败', cls: 'text-press' }, } const SUFFICIENCY_LABEL: Record = { @@ -664,32 +597,32 @@ const SUFFICIENCY_LABEL: Record = { none: '无', } -function AgentCard({ report: r }: { report: AgentReport }) { - const badge = STATUS_BADGE[r.status] ?? { label: r.status, cls: 'bg-ink-100 text-ink-500 border-ink-200' } +/** 单路专家意见:汉字编号 + 细线行 */ +function AgentCard({ report: r, no }: { report: AgentReport; no: string }) { + const badge = STATUS_BADGE[r.status] ?? { label: r.status, cls: 'text-ink-400' } const inactive = r.status !== 'ok' return ( -
- - +
+ + {no} + {AGENT_LABELS[r.agent] ?? r.agent} + {badge.label} - {AGENT_LABELS[r.agent] ?? r.agent} - - {badge.label} - - + {r.status === 'ok' && r.subjective_confidence !== null && ( 信心 {Math.round(r.subjective_confidence * 100)}% )} {r.status === 'ok' && r.probable_score && ( - {r.probable_score} + {r.probable_score} )} + -
+
{/* 无数据 / 失败时给出明确说明,避免用户以为是空白 bug */} {inactive && (

@@ -701,9 +634,9 @@ function AgentCard({ report: r }: { report: AgentReport }) { {!inactive && r.home_edge !== null && (

-
- 主队优势 - 0 ? 'text-brand-600' : r.home_edge < 0 ? 'text-amber-600' : 'text-ink-500'}`}> +
+ 主队优势 + 0 ? 'text-press' : r.home_edge < 0 ? 'text-ink-700' : 'text-ink-500'}`}> {r.home_edge > 0 ? '+' : ''}{r.home_edge.toFixed(2)}
@@ -716,14 +649,14 @@ function AgentCard({ report: r }: { report: AgentReport }) { )} {r.analysis && ( -

{r.analysis}

+

{r.analysis}

)} {r.key_evidence.length > 0 && (
    {r.key_evidence.map((e, i) => (
  • - + {e}
  • ))} @@ -731,20 +664,18 @@ function AgentCard({ report: r }: { report: AgentReport }) { )} {r.exp_home_goals !== null && r.exp_away_goals !== null && ( -
    - 进球期望 - - {r.exp_home_goals.toFixed(1)} - {r.exp_away_goals.toFixed(1)} - -
    +

    + 进球期望 {r.exp_home_goals.toFixed(1)} - {r.exp_away_goals.toFixed(1)} +

    )} {!inactive && ( -
    - 数据充分度 {SUFFICIENCY_LABEL[r.data_sufficiency] ?? r.data_sufficiency} +

    + 数据充分度 {SUFFICIENCY_LABEL[r.data_sufficiency] ?? r.data_sufficiency} + | {r.model} - {r.latency_ms !== null && {r.latency_ms}ms} -

    + {r.latency_ms !== null && {r.latency_ms}ms} +

    )}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 19b5424..2217c23 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -4,79 +4,57 @@ export default { theme: { extend: { colors: { - // 品牌主色:偏冷的蓝,比默认 blue-600 更沉稳,适合数据产品 - brand: { - 50: '#EFF6FF', - 100: '#DBEAFE', - 200: '#BFDBFE', - 300: '#93C5FD', - 400: '#60A5FA', - 500: '#3B82F6', - 600: '#2563EB', - 700: '#1D4ED8', - 800: '#1E40AF', - 900: '#1E3A8A', - 950: '#172554', + // 纸白:微暖底色,像新闻纸而不是纯白画布 + paper: { + 50: '#FDFCF8', + 100: '#F7F4EC', + 200: '#EDE9DE', + 300: '#DDD7C7', }, - // 中性色:带一点冷调,避免纯灰显得脏 + // 墨色:暖黑灰阶,替代冷调 slate ink: { - 50: '#F8FAFC', - 100: '#F1F5F9', - 200: '#E2E8F0', - 300: '#CBD5E1', - 400: '#94A3B8', - 500: '#64748B', - 600: '#475569', - 700: '#334155', - 800: '#1E293B', - 900: '#0F172A', + 50: '#FAF9F7', + 100: '#F0EEE9', + 200: '#E2DFD7', + 300: '#C9C4B8', + 400: '#9C9587', + 500: '#6E675B', + 600: '#524C42', + 700: '#3B362E', + 800: '#282420', + 900: '#17140F', + }, + // 印报红:全站唯一强调色,克制使用 + press: { + DEFAULT: '#9E1B1B', + dark: '#7C1414', + wash: '#F7E9E4', }, - // 语义色:状态徽章 / 提示条统一走这套 - win: { bg: '#ECFDF5', fg: '#047857', line: '#A7F3D0' }, - draw: { bg: '#FFFBEB', fg: '#B45309', line: '#FDE68A' }, - loss: { bg: '#FEF2F2', fg: '#B91C1C', line: '#FECACA' }, }, fontFamily: { + // 标题与比分:宋体血统,报纸版面的骨架 + serif: [ + 'Georgia', + '"Times New Roman"', + '"Songti SC"', + 'STSong', + 'SimSun', + '"Noto Serif CJK SC"', + 'serif', + ], + // 正文:中文黑体栈 sans: [ - 'ui-sans-serif', - '-apple-system', - 'BlinkMacSystemFont', - '"Segoe UI"', '"PingFang SC"', '"Hiragino Sans GB"', '"Microsoft YaHei"', + '"Noto Sans CJK SC"', 'sans-serif', ], mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'], }, fontSize: { - // 多一档 2xs,给徽章/元信息用 '2xs': ['11px', { lineHeight: '16px' }], }, - borderRadius: { - card: '12px', - pill: '9999px', - }, - boxShadow: { - // 卡片阴影:极浅,靠层次而非重影提升 - card: '0 1px 2px 0 rgb(15 23 42 / 0.04), 0 1px 3px 0 rgb(15 23 42 / 0.06)', - raise: '0 4px 12px -2px rgb(15 23 42 / 0.08), 0 2px 6px -2px rgb(15 23 42 / 0.04)', - focus: '0 0 0 3px rgb(37 99 235 / 0.15)', - }, - keyframes: { - 'fade-up': { - '0%': { opacity: '0', transform: 'translateY(4px)' }, - '100%': { opacity: '1', transform: 'translateY(0)' }, - }, - shimmer: { - '0%': { backgroundPosition: '-200% 0' }, - '100%': { backgroundPosition: '200% 0' }, - }, - }, - animation: { - 'fade-up': 'fade-up 0.22s ease-out both', - shimmer: 'shimmer 1.4s linear infinite', - }, }, }, plugins: [],