보스 수익 계산기 페이지 테마 토큰화

- Select/Tooltip 공통 컴포넌트 테마 대응
- BossCrystal 루트/CharacterPanel/BossSelector 전체 이관
- 비활성 row/난이도 버튼 테마별 처리 (--disabled-opacity, --inactive-filter)
- 라이트 테마 warning 색상 갈색 → 주황 계열로 조정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-18 12:15:04 +09:00
parent f89d13431a
commit e0dd7d1bc4
6 changed files with 320 additions and 73 deletions

View file

@ -61,35 +61,49 @@ export default function Select({ value, onChange, options, disabled, className =
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: flipUp ? 6 : -6, scale: 0.98 }}
transition={{ duration: 0.15 }}
className={`fixed z-[100] rounded-lg border border-white/10 bg-gray-900 text-white shadow-xl overflow-hidden ${
className={`fixed z-[100] rounded-lg border overflow-hidden ${
flipUp ? 'origin-bottom' : 'origin-top'
}`}
style={
flipUp
style={{
background: 'var(--popup-bg)',
borderColor: 'var(--popup-border)',
boxShadow: 'var(--popup-shadow)',
color: 'var(--text-strong)',
...(flipUp
? { bottom: pos.bottomOffset + 4, left: pos.left, minWidth: pos.width }
: { top: pos.top + 4, left: pos.left, minWidth: pos.width }
}
),
}}
>
<div className="max-h-60 overflow-y-auto py-1">
{options.map((opt) => (
{options.map((opt) => {
const isActive = opt.value === value
return (
<button
key={opt.value}
type="button"
onClick={() => { onChange(opt.value); setOpen(false) }}
className={`w-full text-left px-3 py-2.5 text-sm transition flex items-center gap-2 ${
opt.value === value
? 'bg-emerald-500/10 text-emerald-300'
: 'hover:bg-white/5'
}`}
className="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2"
style={isActive ? {
background: 'var(--option-selected-bg)',
color: 'var(--option-selected-text)',
} : undefined}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.background = 'var(--row-hover-bg)'
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.background = ''
}}
>
{opt.value === value && (
{isActive && (
<svg className="w-3 h-3 shrink-0" viewBox="0 0 12 12" fill="none">
<path d="M2.5 6L5 8.5L9.5 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
<span className={opt.value !== value ? 'pl-5' : ''}>{opt.label}</span>
<span className={!isActive ? 'pl-5' : ''}>{opt.label}</span>
</button>
))}
)
})}
</div>
</motion.div>
)}
@ -103,14 +117,24 @@ export default function Select({ value, onChange, options, disabled, className =
type="button"
disabled={disabled}
onClick={() => !disabled && setOpen((v) => !v)}
className={`w-full flex items-center justify-between gap-2 rounded-lg border bg-gray-950 px-3 py-2 text-sm transition outline-none ${
open ? 'border-emerald-500/50' : 'border-white/10 hover:border-white/20'
} ${disabled ? 'opacity-50 !cursor-default' : ''}`}
className={`w-full flex items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm outline-none ${
disabled ? 'opacity-50 !cursor-default' : ''
}`}
style={{
background: 'var(--input-bg)',
borderColor: open ? 'var(--input-border-focus)' : 'var(--input-border)',
color: 'var(--text-strong)',
}}
>
<span className={selected ? '' : 'text-gray-500'}>
<span style={{ color: selected ? 'var(--text-strong)' : 'var(--input-placeholder)' }}>
{selected ? selected.label : placeholder}
</span>
<svg className={`w-3.5 h-3.5 text-gray-500 transition ${open ? 'rotate-180' : ''}`} viewBox="0 0 12 12" fill="none">
<svg
className={`w-3.5 h-3.5 transition ${open ? 'rotate-180' : ''}`}
style={{ color: 'var(--input-icon)' }}
viewBox="0 0 12 12"
fill="none"
>
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>

View file

@ -98,8 +98,11 @@ export default function Tooltip({ text, children, placement = 'top', delay = 200
zIndex: 9999,
opacity: coords ? 1 : 0,
transition: 'opacity 120ms ease-out',
background: 'var(--tooltip-bg)',
color: 'var(--tooltip-text)',
borderColor: 'var(--tooltip-border)',
}}
className="pointer-events-none px-2 py-1 rounded-md bg-gray-900 border border-white/10 text-xs text-gray-200 shadow-lg whitespace-nowrap"
className="pointer-events-none px-2 py-1 rounded-md border text-xs shadow-lg whitespace-nowrap"
>
{text}
</div>,

View file

@ -72,12 +72,26 @@ export default function BossCrystal() {
return (
<div className="h-full">
{isLoading ? (
<div className="rounded-2xl border border-white/5 bg-gray-900/40 p-16 text-center">
<div className="w-6 h-6 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin mx-auto" />
<div
className="rounded-2xl border p-16 text-center"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--panel-border)',
boxShadow: 'var(--panel-shadow)',
}}
>
<div className="w-6 h-6 border-2 border-t-transparent rounded-full animate-spin mx-auto" style={{ borderColor: 'var(--accent)', borderTopColor: 'transparent' }} />
</div>
) : (
<div className="grid gap-4 lg:grid-cols-[420px_1fr] h-full min-h-0">
<div className="rounded-2xl border border-white/5 bg-gray-900/40 p-4 min-h-0 max-h-full self-start overflow-hidden flex flex-col">
<div
className="rounded-2xl border p-4 min-h-0 max-h-full self-start overflow-hidden flex flex-col"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--panel-border)',
boxShadow: 'var(--panel-shadow)',
}}
>
<CharacterPanel
characters={characters}
selectedName={selectedChar}

View file

@ -6,7 +6,14 @@ const LABEL_EN = { easy: 'EASY', normal: 'NORMAL', hard: 'HARD', chaos: 'CHAOS',
export default function BossSelector({ characterName, bosses, selections, onChange, maxReached, selectedCount, maxPerCharacter }) {
if (!characterName) {
return (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-16 text-center text-sm text-gray-500">
<div
className="rounded-2xl border border-dashed p-16 text-center text-sm"
style={{
borderColor: 'var(--dashed-border)',
background: 'var(--skeleton-bg)',
color: 'var(--text-dim)',
}}
>
좌측에서 캐릭터를 선택해주세요
</div>
)
@ -14,16 +21,37 @@ export default function BossSelector({ characterName, bosses, selections, onChan
if (bosses.length === 0) {
return (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-16 text-center text-sm text-gray-500">
<div
className="rounded-2xl border border-dashed p-16 text-center text-sm"
style={{
borderColor: 'var(--dashed-border)',
background: 'var(--skeleton-bg)',
color: 'var(--text-dim)',
}}
>
등록된 보스가 없습니다
</div>
)
}
return (
<div className="rounded-xl border border-white/5 bg-gray-900/40 overflow-hidden flex flex-col h-full">
<div
className="rounded-xl border overflow-hidden flex flex-col h-full"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--panel-border)',
boxShadow: 'var(--panel-shadow)',
}}
>
{/* 헤더 (고정) */}
<div className="flex items-center gap-3 px-3 py-3 bg-gray-950/60 border-b border-white/5 text-base font-semibold text-gray-300 shrink-0">
<div
className="flex items-center gap-3 px-3 py-3 border-b text-base font-medium shrink-0"
style={{
background: 'var(--surface-2)',
borderColor: 'var(--panel-border)',
color: 'var(--text-emphasis)',
}}
>
<div className="w-52 shrink-0">보스</div>
<div className="flex-1">난이도</div>
<div className="w-20 shrink-0 text-center">파티원 </div>
@ -31,7 +59,7 @@ export default function BossSelector({ characterName, bosses, selections, onChan
</div>
{/* 목록 (스크롤) */}
<div className="flex-1 overflow-y-auto min-h-0">
<div className="divide-y divide-white/5">
<div className="divide-y" style={{ '--tw-divide-opacity': 1 }}>
{bosses.map((boss) => {
const availableDiffs = DIFFICULTIES.filter((d) =>
boss.difficulties.some((bd) => bd.difficulty === d.key)
@ -52,13 +80,20 @@ export default function BossSelector({ characterName, bosses, selections, onChan
return (
<div
key={boss.id}
className={`flex items-center gap-3 px-3 py-3 transition ${
disabled ? 'opacity-30 pointer-events-none' : ''
className={`flex items-center gap-3 px-3 py-3 border-t first:border-t-0 ${
disabled ? 'pointer-events-none' : ''
}`}
style={{
borderColor: 'var(--panel-border)',
opacity: disabled ? 'var(--disabled-opacity)' : 1,
}}
>
{/* 보스 이미지 + 이름 */}
<div className="flex items-center gap-2.5 w-52 shrink-0">
<div className="shrink-0 w-11 h-11 rounded-lg bg-gray-900 overflow-hidden">
<div
className="shrink-0 w-11 h-11 rounded-lg overflow-hidden"
style={{ background: 'var(--surface-nested)' }}
>
<img src={boss.image_url || '/default.png'} alt={boss.name} className="w-full h-full object-cover" />
</div>
<span className="text-base font-medium leading-tight whitespace-nowrap overflow-hidden text-ellipsis">{boss.name}</span>
@ -72,7 +107,7 @@ export default function BossSelector({ characterName, bosses, selections, onChan
background: d.colors.bg,
borderColor: d.colors.border,
color: d.colors.text,
filter: active ? 'none' : 'brightness(0.4)',
filter: active ? 'none' : 'var(--inactive-filter)',
}
return (
<button
@ -103,12 +138,20 @@ export default function BossSelector({ characterName, bosses, selections, onChan
align="right"
/>
) : (
<div className="text-xs text-gray-700 text-center">-</div>
<div
className="text-xs text-center"
style={{ color: 'var(--text-dim)' }}
>
-
</div>
)}
</div>
{/* 수익 */}
<div className={`w-32 shrink-0 text-right text-sm font-medium tabular-nums ${sel ? 'text-emerald-300' : 'text-gray-700'}`}>
<div
className="w-32 shrink-0 text-right text-sm font-medium tabular-nums"
style={{ color: sel ? 'var(--accent-bright)' : 'var(--text-dim)' }}
>
{sel ? formatMeso(revenue) : '-'}
</div>
</div>

View file

@ -48,14 +48,16 @@ function CharacterContent({ char, selections, bosses }) {
draggable={false}
/>
) : (
<span className="text-gray-700 text-4xl">?</span>
<span className="text-4xl" style={{ color: 'var(--text-dim)' }}>?</span>
)}
</div>
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-baseline gap-2 min-w-0">
<span className="text-base font-semibold truncate">{char.character_name}</span>
<span className="text-xs text-gray-500 truncate">Lv.{char.character_level} · {char.job_name}</span>
<span className="text-xs truncate" style={{ color: 'var(--text-dim)' }}>
Lv.{char.character_level} · {char.job_name}
</span>
</div>
{visibleBosses.length > 0 ? (
@ -68,7 +70,13 @@ function CharacterContent({ char, selections, bosses }) {
text={`${diff?.label || ''} ${item.boss.name} · ${formatMeso(item.revenue)}`}
>
<div className="space-y-0.5">
<div className="aspect-square rounded bg-gray-900 overflow-hidden border border-white/5">
<div
className="aspect-square rounded overflow-hidden border"
style={{
background: 'var(--surface-nested)',
borderColor: 'var(--panel-border)',
}}
>
<img src={item.boss.image_url || '/default.png'} alt="" draggable={false} className="w-full h-full object-cover select-none" />
</div>
<div className="flex justify-center">
@ -85,17 +93,38 @@ function CharacterContent({ char, selections, bosses }) {
})}
</div>
) : (
<div className="text-xs text-gray-600 italic h-[58px] flex items-center">보스 미선택</div>
<div
className="text-xs italic h-[58px] flex items-center"
style={{ color: 'var(--text-dim)' }}
>
보스 미선택
</div>
)}
</div>
</div>
<div className="flex items-center justify-between border-t border-white/5 pt-2">
<div
className="flex items-center justify-between border-t pt-2"
style={{ borderColor: 'var(--panel-border)' }}
>
<div className="flex items-baseline gap-1 tabular-nums">
<span className={`text-base font-bold ${count > 0 ? 'text-amber-300' : 'text-gray-600'}`}>{count}</span>
<span className="text-base font-bold text-amber-300/40">/ {MAX_PER_CHARACTER}</span>
<span
className="text-base font-bold"
style={{ color: count > 0 ? 'var(--warning-text-bright)' : 'var(--text-dim)' }}
>
{count}
</span>
<span
className="text-base font-bold"
style={{ color: count > 0 ? 'var(--warning-text-dim)' : 'var(--text-dim)' }}
>
/ {MAX_PER_CHARACTER}
</span>
</div>
<div className={`text-sm font-semibold tabular-nums whitespace-nowrap ${count > 0 ? 'text-emerald-300' : 'text-gray-700'}`}>
<div
className="text-sm font-semibold tabular-nums whitespace-nowrap"
style={{ color: count > 0 ? 'var(--accent-bright)' : 'var(--text-dim)' }}
>
{count > 0 ? formatMeso(totalRevenue) : '-'}
</div>
</div>
@ -122,17 +151,17 @@ function CharacterItem({ char, isSelected, selections, bosses, onSelect, onRemov
if (e.target.closest('button')) return
onSelect(char.character_name)
}}
className={`group relative rounded-xl border cursor-pointer select-none ${
isSelected
? 'border-emerald-500/40 bg-emerald-500/[0.08]'
: 'border-white/5 hover:border-white/15 bg-gray-950/40 hover:bg-gray-950/60'
}`}
className="group relative rounded-xl border cursor-pointer select-none"
style={{
borderColor: isSelected ? 'var(--selected-border)' : 'var(--panel-border)',
background: isSelected ? 'var(--selected-bg)' : 'var(--surface-3)',
}}
>
{/* 드래그 핸들 */}
<div
onPointerDown={(e) => { e.preventDefault(); dragControls.start(e) }}
className="absolute left-0 top-0 bottom-0 w-8 flex items-center justify-center text-gray-600 hover:text-gray-400 cursor-grab active:cursor-grabbing"
style={{ touchAction: 'none' }}
className="absolute left-0 top-0 bottom-0 w-8 flex items-center justify-center cursor-grab active:cursor-grabbing"
style={{ touchAction: 'none', color: 'var(--text-dim)' }}
>
<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
<circle cx="3" cy="3" r="1.2" />
@ -147,7 +176,8 @@ function CharacterItem({ char, isSelected, selections, bosses, onSelect, onRemov
<button
type="button"
onClick={(e) => { e.stopPropagation(); onRemove(char) }}
className="absolute top-2 right-2 z-10 w-6 h-6 rounded text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition opacity-0 group-hover:opacity-100 flex items-center justify-center text-base"
className="absolute top-2 right-2 z-10 w-6 h-6 rounded opacity-0 group-hover:opacity-100 flex items-center justify-center text-base hover:bg-[var(--danger-bg-hover)] hover:text-[var(--danger-text)]"
style={{ color: 'var(--text-dim)' }}
aria-label="삭제"
>
×
@ -222,13 +252,20 @@ export default function CharacterPanel({
return (
<div className="flex flex-col gap-4 min-h-0 flex-1">
{/* 총 수익 카드 (고정) */}
<div className="rounded-2xl border border-emerald-500/30 bg-gradient-to-br from-emerald-500/15 to-emerald-500/5 p-4 space-y-3 shrink-0">
<div
className="rounded-2xl border p-4 space-y-3 shrink-0"
style={{
borderColor: 'var(--selected-border)',
background: 'var(--selected-bg)',
}}
>
<div>
<div className="text-xs text-emerald-200/80"> 주간 수익</div>
<div className="text-xs" style={{ color: 'var(--accent-bright)' }}> 주간 수익</div>
<div ref={totalContainerRef} className="mt-1 overflow-hidden">
<div
ref={totalTextRef}
className="font-bold text-emerald-300 leading-tight whitespace-nowrap inline-block"
className="font-bold leading-tight whitespace-nowrap inline-block"
style={{ color: 'var(--accent-bright)' }}
>
{totalText}
</div>
@ -237,25 +274,42 @@ export default function CharacterPanel({
<div className="grid grid-cols-[1fr_auto] gap-x-3 items-center">
<div className="space-y-2">
<div className="text-sm text-gray-400"> 결정 개수</div>
<div className="h-2 rounded-full bg-gray-900 overflow-hidden">
<div className="text-sm" style={{ color: 'var(--text-muted)' }}> 결정 개수</div>
<div
className={`h-full transition-all ${totalCount > MAX_PER_ACCOUNT ? 'bg-amber-500' : 'bg-emerald-500'}`}
style={{ width: `${usagePct}%` }}
className="h-2 rounded-full overflow-hidden"
style={{ background: 'var(--progress-track)' }}
>
<div
className="h-full transition-all"
style={{
width: `${usagePct}%`,
background: totalCount > MAX_PER_ACCOUNT ? 'var(--progress-amber)' : 'var(--progress-emerald)',
}}
/>
</div>
</div>
<div className="flex items-baseline gap-1 tabular-nums">
<span className={`text-2xl font-bold leading-none ${totalCount > MAX_PER_ACCOUNT ? 'text-red-400' : 'text-amber-300'}`}>
<span
className="text-2xl font-bold leading-none"
style={{ color: totalCount > MAX_PER_ACCOUNT ? 'var(--danger-text)' : 'var(--warning-text-bright)' }}
>
{accountUsage}
</span>
<span className={`text-2xl font-bold leading-none ${totalCount > MAX_PER_ACCOUNT ? 'text-red-400/40' : 'text-amber-300/40'}`}>
<span
className="text-2xl font-bold leading-none"
style={{
color: totalCount > MAX_PER_ACCOUNT ? 'var(--danger-text)' : 'var(--warning-text-dim)',
opacity: totalCount > MAX_PER_ACCOUNT ? 0.4 : 1,
}}
>
/ {MAX_PER_ACCOUNT}
</span>
</div>
</div>
{totalCount > MAX_PER_ACCOUNT && (
<p className="text-[10px] text-amber-400"> 한도 {totalCount - MAX_PER_ACCOUNT} 초과</p>
<p className="text-[10px]" style={{ color: 'var(--warning-text)' }}>
한도 {totalCount - MAX_PER_ACCOUNT} 초과
</p>
)}
</div>
@ -263,7 +317,10 @@ export default function CharacterPanel({
<div className="shrink-0">
<form onSubmit={handleSubmit} className="flex gap-2">
<div className="relative flex-1 min-w-0">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none">
<span
className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"
style={{ color: 'var(--input-icon)' }}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" strokeWidth="1.5" />
<path d="M10 10L14 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
@ -274,18 +331,30 @@ export default function CharacterPanel({
value={name}
onChange={(e) => { setName(e.target.value); if (error) setError('') }}
placeholder="캐릭터 닉네임 검색"
className="w-full rounded-lg border-2 border-white/10 bg-gray-950 pl-10 pr-3 py-2.5 text-sm outline-none focus:border-emerald-500/60 hover:border-white/20 transition"
className="w-full rounded-lg border-2 pl-10 pr-3 py-2.5 text-sm outline-none transition focus:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
style={{
background: 'var(--input-bg)',
borderColor: 'var(--input-border)',
color: 'var(--text-strong)',
}}
/>
</div>
<button
type="submit"
disabled={searchMutation.isPending}
className="rounded-lg bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 px-5 py-2.5 text-sm font-medium transition shrink-0 shadow-lg shadow-emerald-500/20"
className="rounded-lg disabled:opacity-50 px-5 py-2.5 text-sm font-medium shrink-0 hover:bg-[var(--btn-primary-bg-hover)]"
style={{
background: 'var(--btn-primary-bg)',
color: 'var(--btn-primary-text)',
boxShadow: 'var(--btn-primary-shadow)',
}}
>
{searchMutation.isPending ? '...' : '추가'}
</button>
</form>
{error && <p className="text-xs text-red-400 mt-1.5">{error}</p>}
{error && (
<p className="text-xs mt-1.5" style={{ color: 'var(--danger-text)' }}>{error}</p>
)}
</div>
{/* 캐릭터 목록 (스크롤) */}

View file

@ -76,6 +76,53 @@
--maintenance-text: #fbbf24;
--dashed-border: rgba(255, 255, 255, 0.1);
--surface-2: rgba(2, 6, 23, 0.6);
--surface-3: rgba(17, 24, 39, 0.4);
--surface-nested: #0f172a;
--input-bg: #020617;
--input-border: rgba(255, 255, 255, 0.1);
--input-border-hover: rgba(255, 255, 255, 0.2);
--input-border-focus: rgba(16, 185, 129, 0.5);
--input-placeholder: #6b7280;
--input-icon: #6b7280;
--selected-bg: rgba(16, 185, 129, 0.08);
--selected-border: rgba(16, 185, 129, 0.4);
--option-selected-bg: rgba(16, 185, 129, 0.12);
--option-selected-text: #6ee7b7;
--btn-primary-bg: #059669;
--btn-primary-bg-hover: #10b981;
--btn-primary-text: #ffffff;
--btn-primary-shadow: 0 4px 14px rgba(16, 185, 129, 0.2);
--danger-text: #f87171;
--danger-text-strong: #dc2626;
--danger-bg-hover: rgba(239, 68, 68, 0.1);
--warning-text: #fbbf24;
--warning-text-bright: #fcd34d;
--warning-text-dim: rgba(252, 211, 77, 0.4);
--progress-track: #0f172a;
--progress-emerald: #10b981;
--progress-amber: #f59e0b;
--accent-bright: #6ee7b7;
--accent-muted: rgba(16, 185, 129, 0.1);
--accent-text-on-emerald: #ecfdf5;
--tooltip-bg: #111827;
--tooltip-border: rgba(255, 255, 255, 0.1);
--tooltip-text: #e5e7eb;
--popup-bg: #111827;
--popup-border: rgba(255, 255, 255, 0.1);
--popup-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
--disabled-opacity: 0.3;
--inactive-filter: brightness(0.4);
}
/* 테마 토큰 - light */
@ -148,6 +195,53 @@
--maintenance-text: #b45309;
--dashed-border: rgba(0, 0, 0, 0.12);
--surface-2: #f8fafc;
--surface-3: #f9fafb;
--surface-nested: #f3f4f6;
--input-bg: #ffffff;
--input-border: rgba(0, 0, 0, 0.12);
--input-border-hover: rgba(0, 0, 0, 0.22);
--input-border-focus: rgba(5, 150, 105, 0.55);
--input-placeholder: #9ca3af;
--input-icon: #94a3b8;
--selected-bg: rgba(16, 185, 129, 0.08);
--selected-border: rgba(5, 150, 105, 0.5);
--option-selected-bg: rgba(16, 185, 129, 0.12);
--option-selected-text: #047857;
--btn-primary-bg: #059669;
--btn-primary-bg-hover: #047857;
--btn-primary-text: #ffffff;
--btn-primary-shadow: 0 4px 14px rgba(16, 185, 129, 0.25);
--danger-text: #dc2626;
--danger-text-strong: #b91c1c;
--danger-bg-hover: rgba(220, 38, 38, 0.08);
--warning-text: #c2410c;
--warning-text-bright: #ea580c;
--warning-text-dim: rgba(234, 88, 12, 0.4);
--progress-track: #e5e7eb;
--progress-emerald: #10b981;
--progress-amber: #f59e0b;
--accent-bright: #047857;
--accent-muted: rgba(16, 185, 129, 0.1);
--accent-text-on-emerald: #ecfdf5;
--tooltip-bg: #111827;
--tooltip-border: rgba(255, 255, 255, 0.08);
--tooltip-text: #f3f4f6;
--popup-bg: #ffffff;
--popup-border: rgba(0, 0, 0, 0.1);
--popup-shadow: 0 10px 30px rgba(15, 23, 42, 0.15);
--disabled-opacity: 0.5;
--inactive-filter: opacity(0.25);
}
html, body, #root {