feat: create professional, polished UI with perfect spacing

- Add generous padding and spacing throughout (p-10, p-12)
- Constrain content width with max-w-4xl and max-w-5xl
- Increase font sizes for better hierarchy (text-3xl, text-lg)
- Add gradient backgrounds to cards and elements
- Implement smooth transitions and hover states
- Create professional progress stepper with labels
- Add shadow effects with color-matched glows
- Increase button padding and font sizes
- Add gradient borders on hover
- Implement proper visual breathing room
- Create polished, premium aesthetic
- Add smooth cubic-bezier transitions
- Improve form input styling with focus rings
- Create professional card hover effects
This commit is contained in:
2025-12-09 13:29:48 +00:00
parent 2f200aa61b
commit 4a53c897c5
6 changed files with 303 additions and 278 deletions

View File

@@ -158,22 +158,22 @@ function App() {
const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id)) const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id))
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100"> <div className="min-h-screen">
<div className="container mx-auto py-8 px-4 sm:py-12 sm:px-6 lg:px-8 max-w-5xl"> <div className="container mx-auto py-12 px-4 sm:px-6 lg:px-8">
{/* Header */} {/* Header */}
<div className="text-center mb-12"> <div className="text-center mb-16 max-w-3xl mx-auto">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl mb-6 shadow-lg"> <div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-3xl mb-8 shadow-xl shadow-indigo-500/30">
<span className="text-3xl">🎧</span> <span className="text-4xl">🎧</span>
</div> </div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-gray-900 mb-4"> <h1 className="text-5xl sm:text-6xl md:text-7xl font-black text-gray-900 mb-6 tracking-tight">
Text to Audiobook Text to Audiobook
</h1> </h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto"> <p className="text-xl text-gray-600 leading-relaxed">
Transform your stories into immersive audio experiences with AI-powered voices Transform your stories into immersive audio experiences with AI-powered voices
</p> </p>
{/* Progress Steps */} {/* Progress Steps */}
<div className="mt-10 flex justify-center items-center gap-3"> <div className="mt-12 flex justify-center items-center gap-2">
{['Input', 'Voices', 'Review', 'Done'].map((step, index) => { {['Input', 'Voices', 'Review', 'Done'].map((step, index) => {
const phases = ['input', 'assignment', 'review', 'generated']; const phases = ['input', 'assignment', 'review', 'generated'];
const isActive = phase === phases[index]; const isActive = phase === phases[index];
@@ -181,15 +181,20 @@ function App() {
return ( return (
<div key={step} className="flex items-center"> <div key={step} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full font-semibold text-sm transition-all ${ <div className="flex flex-col items-center">
isActive ? 'bg-indigo-600 text-white scale-110' : <div className={`flex items-center justify-center w-12 h-12 rounded-full font-bold text-sm transition-all duration-300 ${
isPast ? 'bg-green-500 text-white' : isActive ? 'bg-indigo-600 text-white scale-110 shadow-lg shadow-indigo-500/50' :
'bg-gray-200 text-gray-500' isPast ? 'bg-green-500 text-white' :
}`}> 'bg-gray-200 text-gray-500'
{isPast ? '✓' : index + 1} }`}>
{isPast ? '✓' : index + 1}
</div>
<span className={`text-xs font-semibold mt-2 ${isActive ? 'text-indigo-600' : 'text-gray-500'}`}>
{step}
</span>
</div> </div>
{index < 3 && ( {index < 3 && (
<div className={`w-12 h-1 mx-2 rounded ${isPast ? 'bg-green-500' : 'bg-gray-200'}`}></div> <div className={`w-16 h-1 mx-3 rounded-full transition-all duration-300 ${isPast || isActive ? 'bg-indigo-600' : 'bg-gray-200'}`}></div>
)} )}
</div> </div>
); );

View File

@@ -8,48 +8,50 @@ export default function AudioPlayerSection({
onDownload onDownload
}: AudioPlayerSectionProps) { }: AudioPlayerSectionProps) {
return ( return (
<div className="card-modern p-8 space-y-6"> <div className="max-w-4xl mx-auto">
{/* Success Header */} <div className="card-modern p-10 sm:p-12 space-y-10">
<div className="text-center"> {/* Success Header */}
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-green-400 to-emerald-500 rounded-2xl mb-4"> <div className="text-center">
<span className="text-3xl">🎉</span> <div className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-green-400 to-emerald-500 rounded-3xl mb-6 shadow-xl shadow-green-500/30">
<span className="text-5xl">🎉</span>
</div>
<h2 className="text-4xl font-black text-gray-900 mb-4">
Your Audiobook is Ready!
</h2>
<p className="text-xl text-gray-600">
Listen below or download to enjoy offline
</p>
</div>
{/* Audio Player */}
<div className="p-8 bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 rounded-2xl border-2 border-indigo-200">
<audio
controls
src={audioUrl}
className="w-full"
preload="metadata"
>
Your browser does not support the audio element.
</audio>
</div> </div>
<h2 className="text-3xl font-bold text-gray-900 mb-2">
Your Audiobook is Ready!
</h2>
<p className="text-gray-600">
Listen below or download to enjoy offline
</p>
</div>
{/* Audio Player */}
<div className="p-6 bg-gradient-to-br from-indigo-50 to-purple-50 rounded-xl border border-indigo-100">
<audio
controls
src={audioUrl}
className="w-full"
preload="metadata"
>
Your browser does not support the audio element.
</audio>
</div>
{/* Download Button */} {/* Download Button */}
<div className="flex justify-center"> <div className="flex justify-center">
<button <button
onClick={onDownload} onClick={onDownload}
className="btn-primary flex items-center gap-2" className="btn-primary flex items-center gap-3"
> >
<span>⬇️</span> <span className="text-xl">⬇️</span>
Download Audiobook Download Audiobook
</button> </button>
</div> </div>
{/* Success Message */} {/* Success Message */}
<div className="p-4 bg-green-50 border border-green-200 text-green-800 rounded-xl text-center"> <div className="p-6 bg-green-50 border-2 border-green-200 text-green-900 rounded-2xl text-center">
<p className="font-medium"> <p className="font-bold text-lg">
✅ Your audiobook has been successfully generated! ✅ Your audiobook has been successfully generated!
</p> </p>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -22,83 +22,85 @@ export default function ScriptReviewSection({
}; };
return ( return (
<div className="card-modern p-8 space-y-6"> <div className="max-w-5xl mx-auto">
<div> <div className="card-modern p-10 sm:p-12 space-y-10">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Review Script</h2> <div>
<p className="text-gray-600">Review and adjust speaker assignments before generating your audiobook.</p> <h2 className="text-3xl font-bold text-gray-900 mb-3">Review Script</h2>
</div> <p className="text-lg text-gray-600">Review and adjust speaker assignments before generating your audiobook.</p>
</div>
{/* Script Segments */} {/* Script Segments */}
<div className="space-y-4"> <div className="space-y-6">
{segments.map((segment, index) => ( {segments.map((segment, index) => (
<div <div
key={segment.id} key={segment.id}
className="p-5 bg-gray-50 rounded-xl border border-gray-200" className="p-6 bg-gradient-to-br from-gray-50 to-white rounded-2xl border-2 border-gray-200 hover:border-indigo-300 transition-all duration-200"
> >
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-3"> <div className="flex flex-col sm:flex-row sm:items-center gap-4 mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 text-indigo-700 rounded-lg font-semibold text-sm"> <div className="flex items-center justify-center w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-500/30">
{index + 1} {index + 1}
</div>
<span className="text-sm font-bold text-gray-500 uppercase tracking-wide">Segment</span>
</div>
{/* Speaker Dropdown */}
<div className="flex items-center gap-3 flex-1">
<label className="text-base font-bold text-gray-700 whitespace-nowrap">
Speaker:
</label>
<select
value={segment.speaker}
onChange={(e) => handleSpeakerChange(segment.id, e.target.value)}
className="flex-1 p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none bg-white font-medium transition-all duration-200"
disabled={isGenerating}
>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.label}
</option>
))}
</select>
</div> </div>
<span className="text-xs font-semibold text-gray-500 uppercase">Segment</span>
</div> </div>
{/* Speaker Dropdown */} {/* Segment Text */}
<div className="flex items-center gap-2 flex-1"> <div className="p-5 bg-white rounded-xl border-2 border-gray-200 text-gray-800 whitespace-pre-wrap leading-relaxed text-base">
<label className="text-sm font-semibold text-gray-700 whitespace-nowrap"> {segment.text}
Speaker:
</label>
<select
value={segment.speaker}
onChange={(e) => handleSpeakerChange(segment.id, e.target.value)}
className="flex-1 p-2 border-2 border-gray-200 rounded-lg focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
disabled={isGenerating}
>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.label}
</option>
))}
</select>
</div> </div>
</div> </div>
))}
</div>
{/* Segment Text */} {/* Action Buttons */}
<div className="p-4 bg-white rounded-lg border border-gray-200 text-gray-800 whitespace-pre-wrap leading-relaxed"> <div className="flex flex-col sm:flex-row gap-4 justify-end pt-4">
{segment.text} <button
</div> onClick={onSkipReview}
</div> disabled={isGenerating}
))} className="px-8 py-4 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 hover:border-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
</div> >
Back
{/* Action Buttons */} </button>
<div className="flex flex-col sm:flex-row gap-3 justify-end pt-4">
<button <button
onClick={onSkipReview} onClick={onGenerate}
disabled={isGenerating} disabled={isGenerating}
className="px-6 py-3 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all" className="btn-primary"
> >
Back {isGenerating ? (
</button> <span className="flex items-center gap-3">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
<button Generating...
onClick={onGenerate} </span>
disabled={isGenerating} ) : (
className="btn-primary" <span className="flex items-center gap-3">
> Generate Audiobook
{isGenerating ? ( <span className="text-lg">✨</span>
<span className="flex items-center gap-2"> </span>
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div> )}
Generating... </button>
</span> </div>
) : (
<span className="flex items-center gap-2">
Generate Audiobook
<span>✨</span>
</span>
)}
</button>
</div> </div>
</div> </div>
); );

View File

@@ -36,56 +36,58 @@ export default function TextInputSection({
const isButtonDisabled = disabled || isLoading || text.trim() === ''; const isButtonDisabled = disabled || isLoading || text.trim() === '';
return ( return (
<div className="card-modern p-8"> <div className="max-w-4xl mx-auto">
<div className="mb-6"> <div className="card-modern p-10 sm:p-12">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Enter Your Text</h2> <div className="mb-10">
<p className="text-gray-600">Paste or type your story below. Use patterns like "Name:" to mark different speakers.</p> <h2 className="text-3xl font-bold text-gray-900 mb-3">Enter Your Text</h2>
</div> <p className="text-lg text-gray-600">Paste or type your story below. Use patterns like "Name:" to mark different speakers.</p>
</div>
<div className="mb-6 relative">
<textarea <div className="mb-8 relative">
value={text} <textarea
onChange={handleTextChange} value={text}
placeholder="Example: onChange={handleTextChange}
placeholder="Example:
Sarah: Hello! How are you today? Sarah: Hello! How are you today?
John: I'm doing great, thanks for asking! John: I'm doing great, thanks for asking!
Sarah: That's wonderful to hear." Sarah: That's wonderful to hear."
className="w-full h-80 p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none resize-y text-base leading-relaxed" className="w-full h-96 p-6 border-2 border-gray-200 rounded-2xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none resize-y text-base leading-relaxed transition-all duration-200 bg-gray-50 focus:bg-white"
disabled={disabled || isLoading} disabled={disabled || isLoading}
/> />
<div className="absolute bottom-3 right-3 text-xs text-gray-500 bg-white px-2 py-1 rounded-md border border-gray-200"> <div className="absolute bottom-5 right-5 text-sm text-gray-500 bg-white px-4 py-2 rounded-xl border border-gray-200 shadow-sm font-medium">
{text.length} characters {text.length} characters
</div>
</div>
{showValidation && text.trim() === '' && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 text-red-700 rounded-xl">
<div className="flex items-center gap-2">
<span>⚠️</span>
<p className="font-medium">Please enter some text before proceeding.</p>
</div> </div>
</div> </div>
)}
<div className="flex justify-end"> {showValidation && text.trim() === '' && (
<button <div className="mb-8 p-5 bg-red-50 border-2 border-red-200 text-red-700 rounded-2xl">
onClick={handleNext} <div className="flex items-center gap-3">
disabled={isButtonDisabled} <span className="text-xl">⚠️</span>
className="btn-primary" <p className="font-semibold">Please enter some text before proceeding.</p>
> </div>
{isLoading ? ( </div>
<span className="flex items-center gap-2"> )}
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
Processing... <div className="flex justify-end">
</span> <button
) : ( onClick={handleNext}
<span className="flex items-center gap-2"> disabled={isButtonDisabled}
Continue className="btn-primary"
<span>→</span> >
</span> {isLoading ? (
)} <span className="flex items-center gap-3">
</button> <div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
Processing...
</span>
) : (
<span className="flex items-center gap-3">
Continue
<span className="text-lg">→</span>
</span>
)}
</button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -159,116 +159,118 @@ export default function VoiceAssignmentSection({
const isGenerateDisabled = !allRolesAssigned || isGenerating; const isGenerateDisabled = !allRolesAssigned || isGenerating;
return ( return (
<div className="card-modern p-8 space-y-8"> <div className="max-w-5xl mx-auto">
<div> <div className="card-modern p-10 sm:p-12 space-y-10">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Assign Voices</h2> <div>
<p className="text-gray-600">Choose a voice for each character in your story.</p> <h2 className="text-3xl font-bold text-gray-900 mb-3">Assign Voices</h2>
</div> <p className="text-lg text-gray-600">Choose a voice for each character in your story.</p>
</div>
{/* Voice Assignments */} {/* Voice Assignments */}
<div className="space-y-4"> <div className="space-y-5">
{roles.map((role, index) => ( {roles.map((role, index) => (
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-3 p-4 bg-gray-50 rounded-xl border border-gray-200"> <div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-4 p-6 bg-gradient-to-br from-gray-50 to-white rounded-2xl border-2 border-gray-200 hover:border-indigo-300 transition-all duration-200">
<div className="flex items-center gap-3 sm:w-48"> <div className="flex items-center gap-4 sm:w-56">
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 text-indigo-700 rounded-lg font-semibold text-sm"> <div className="flex items-center justify-center w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-500/30">
{index + 1} {index + 1}
</div>
<label className="font-bold text-gray-900 text-lg">
{role.label}
</label>
</div> </div>
<label className="font-semibold text-gray-900"> <select
{role.label} value={voiceAssignments.get(role.id) || ''}
</label> onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
className="flex-1 p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none bg-white text-base font-medium transition-all duration-200"
>
<option value="">Select a voice...</option>
{KOKORO_VOICES.map((voice) => (
<option key={voice.id} value={voice.id}>
{voice.label}
</option>
))}
</select>
</div> </div>
))}
</div>
{/* Settings Grid */}
<div className="grid sm:grid-cols-2 gap-8 pt-4">
{/* Language */}
<div>
<label className="block text-base font-bold text-gray-900 mb-3">
Language
</label>
<select <select
value={voiceAssignments.get(role.id) || ''} value={language}
onChange={(e) => handleVoiceSelect(role.id, e.target.value)} onChange={handleLanguageSelect}
className="flex-1 p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white" className="w-full p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none bg-white text-base font-medium transition-all duration-200"
> >
<option value="">Select a voice...</option> {LANGUAGES.map((lang) => (
{KOKORO_VOICES.map((voice) => ( <option key={lang.code} value={lang.code}>
<option key={voice.id} value={voice.id}> {lang.label}
{voice.label}
</option> </option>
))} ))}
</select> </select>
</div> </div>
))}
</div>
{/* Settings Grid */} {/* Speed */}
<div className="grid sm:grid-cols-2 gap-6"> <div>
{/* Language */} <label className="block text-base font-bold text-gray-900 mb-3">
<div> Playback Speed: <span className="text-indigo-600">{speed.toFixed(1)}x</span>
<label className="block text-sm font-semibold text-gray-900 mb-2"> </label>
Language <input
</label> type="range"
<select min="0.5"
value={language} max="1.5"
onChange={handleLanguageSelect} step="0.1"
className="w-full p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white" value={speed}
> onChange={handleSpeedChange}
{LANGUAGES.map((lang) => ( className="w-full h-3"
<option key={lang.code} value={lang.code}> />
{lang.label} <div className="flex justify-between text-sm text-gray-500 mt-2 font-medium">
</option> <span>0.5x</span>
))} <span>1.5x</span>
</select> </div>
</div>
{/* Speed */}
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Playback Speed: {speed.toFixed(1)}x
</label>
<input
type="range"
min="0.5"
max="1.5"
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>0.5x</span>
<span>1.5x</span>
</div> </div>
</div> </div>
</div>
{/* Validation Message */} {/* Validation Message */}
{!allRolesAssigned && !isGenerating && ( {!allRolesAssigned && !isGenerating && (
<div className="p-4 bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl"> <div className="p-5 bg-yellow-50 border-2 border-yellow-200 text-yellow-900 rounded-2xl">
<p className="text-sm font-medium">Please assign voices to all roles before continuing.</p> <p className="font-semibold">⚠️ Please assign voices to all roles before continuing.</p>
</div> </div>
)}
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 justify-end">
{onSkipReview && (
<button
onClick={onSkipReview}
disabled={isGenerateDisabled}
className="px-6 py-3 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
Skip Review & Generate
</button>
)} )}
<button
onClick={onGenerate} {/* Action Buttons */}
disabled={isGenerateDisabled} <div className="flex flex-col sm:flex-row gap-4 justify-end pt-4">
className="btn-primary" {onSkipReview && (
> <button
{isGenerating ? ( onClick={onSkipReview}
<span className="flex items-center gap-2"> disabled={isGenerateDisabled}
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div> className="px-8 py-4 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 hover:border-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
Generating... >
</span> Skip Review & Generate
) : ( </button>
<span className="flex items-center gap-2">
Continue
<span>→</span>
</span>
)} )}
</button> <button
onClick={onGenerate}
disabled={isGenerateDisabled}
className="btn-primary"
>
{isGenerating ? (
<span className="flex items-center gap-3">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
Generating...
</span>
) : (
<span className="flex items-center gap-3">
Continue
<span className="text-lg">→</span>
</span>
)}
</button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -2,54 +2,66 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
* {
box-sizing: border-box;
}
:root { :root {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6; line-height: 1.6;
font-weight: 400; font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
body { body {
margin: 0; margin: 0;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background: #f8f9fa; background: linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%);
} }
@layer utilities { @layer utilities {
.card-modern { .card-modern {
background: white; background: white;
border-radius: 16px; border-radius: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
border: 1px solid #e5e7eb; border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.2s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.card-modern:hover { .card-modern:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
transform: translateY(-2px); transform: translateY(-4px);
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white; color: white;
font-weight: 600; font-weight: 600;
padding: 12px 32px; padding: 14px 40px;
border-radius: 12px; border-radius: 14px;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4);
font-size: 16px;
} }
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4); box-shadow: 0 8px 24px rgba(99, 102, 241, 0.5);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
} }
.btn-primary:disabled { .btn-primary:disabled {
opacity: 0.5; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none;
} }
} }