Spaces:
Sleeping
Sleeping
lakshmisravya123 commited on
Commit ·
539a59c
1
Parent(s): 5d94d12
Upgrade: week-by-week roadmap, salary impact, portfolio projects, quick wins
Browse files- .gitignore +1 -1
- backend/services/ai.js +117 -15
- frontend/src/components/Results.jsx +61 -13
- frontend/src/styles/global.css +31 -0
.gitignore
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
node_modules/
|
| 2 |
-
.env
|
| 3 |
dist/
|
|
|
|
| 4 |
.DS_Store
|
|
|
|
| 1 |
node_modules/
|
|
|
|
| 2 |
dist/
|
| 3 |
+
.env
|
| 4 |
.DS_Store
|
backend/services/ai.js
CHANGED
|
@@ -8,7 +8,15 @@ async function callAI(prompt) {
|
|
| 8 |
const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
| 9 |
method: 'POST',
|
| 10 |
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${GROQ_API_KEY}` },
|
| 11 |
-
body: JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
});
|
| 13 |
if (res.ok) { const data = await res.json(); return data.choices[0].message.content; }
|
| 14 |
console.warn('Groq failed, falling back to Ollama...');
|
|
@@ -24,32 +32,126 @@ async function callAI(prompt) {
|
|
| 24 |
|
| 25 |
function parseJSON(text) {
|
| 26 |
try { return JSON.parse(text.trim()); }
|
| 27 |
-
catch {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
async function analyzeGap(jobPosting, userSkills) {
|
| 31 |
-
const text = await callAI(`You are a career development expert and skill gap analyst.
|
| 32 |
|
| 33 |
-
JOB POSTING
|
| 34 |
${jobPosting}
|
| 35 |
|
| 36 |
-
CANDIDATE
|
| 37 |
${userSkills}
|
| 38 |
|
| 39 |
-
|
|
|
|
| 40 |
{
|
| 41 |
"jobTitle": "<extracted job title>",
|
| 42 |
-
"company": "<extracted company or
|
| 43 |
-
"matchScore":
|
| 44 |
-
"
|
| 45 |
-
|
| 46 |
-
"
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
return parseJSON(text);
|
| 54 |
}
|
| 55 |
|
|
|
|
| 8 |
const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
| 9 |
method: 'POST',
|
| 10 |
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${GROQ_API_KEY}` },
|
| 11 |
+
body: JSON.stringify({
|
| 12 |
+
model: GROQ_MODEL,
|
| 13 |
+
messages: [
|
| 14 |
+
{ role: 'system', content: 'You are a world-class career advisor, skill gap analyst, and technical recruiter with 20 years of experience. You provide extremely detailed, actionable analysis. Always return valid JSON only.' },
|
| 15 |
+
{ role: 'user', content: prompt }
|
| 16 |
+
],
|
| 17 |
+
temperature: 0.7,
|
| 18 |
+
max_tokens: 4096
|
| 19 |
+
}),
|
| 20 |
});
|
| 21 |
if (res.ok) { const data = await res.json(); return data.choices[0].message.content; }
|
| 22 |
console.warn('Groq failed, falling back to Ollama...');
|
|
|
|
| 32 |
|
| 33 |
function parseJSON(text) {
|
| 34 |
try { return JSON.parse(text.trim()); }
|
| 35 |
+
catch {
|
| 36 |
+
const m = text.match(/\{[\s\S]*\}/);
|
| 37 |
+
if (m) return JSON.parse(m[0]);
|
| 38 |
+
throw new Error('Failed to parse AI response');
|
| 39 |
+
}
|
| 40 |
}
|
| 41 |
|
| 42 |
async function analyzeGap(jobPosting, userSkills) {
|
| 43 |
+
const text = await callAI(`You are a world-class career development expert, technical recruiter, and skill gap analyst.
|
| 44 |
|
| 45 |
+
=== JOB POSTING ===
|
| 46 |
${jobPosting}
|
| 47 |
|
| 48 |
+
=== CANDIDATE CURRENT SKILLS / RESUME ===
|
| 49 |
${userSkills}
|
| 50 |
|
| 51 |
+
Perform an exhaustive skill gap analysis. Return ONLY valid JSON (no markdown, no code fences) with this EXACT structure:
|
| 52 |
+
|
| 53 |
{
|
| 54 |
"jobTitle": "<extracted job title>",
|
| 55 |
+
"company": "<extracted company or Not specified>",
|
| 56 |
+
"matchScore": 75,
|
| 57 |
+
"verdict": "<Ready to Apply|Almost Ready|Need More Preparation|Significant Gap>",
|
| 58 |
+
|
| 59 |
+
"matchedSkills": [
|
| 60 |
+
{
|
| 61 |
+
"skill": "<skill name>",
|
| 62 |
+
"level": "<strong|moderate|basic>",
|
| 63 |
+
"currentProficiency": "<advanced|intermediate|beginner>",
|
| 64 |
+
"targetProficiency": "<advanced|intermediate|beginner>",
|
| 65 |
+
"note": "<how this skill relates to the job>"
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
|
| 69 |
+
"missingSkills": [
|
| 70 |
+
{
|
| 71 |
+
"skill": "<skill name>",
|
| 72 |
+
"importance": "<critical|important|nice-to-have>",
|
| 73 |
+
"priorityScore": 8,
|
| 74 |
+
"difficulty": "<easy|medium|hard>",
|
| 75 |
+
"timeToLearn": "<e.g. 2 weeks>",
|
| 76 |
+
"currentProficiency": "none",
|
| 77 |
+
"targetProficiency": "<advanced|intermediate|beginner>",
|
| 78 |
+
"salaryImpact": "<estimated annual salary increase in USD if this skill is acquired, e.g. +5000-10000>",
|
| 79 |
+
"industryDemand": "<very high|high|moderate|low>",
|
| 80 |
+
"demandTrend": "<rising|stable|declining>",
|
| 81 |
+
"portfolioProject": "<a specific project idea to demonstrate this skill>"
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
|
| 85 |
+
"salaryAnalysis": {
|
| 86 |
+
"estimatedRangeWithCurrentSkills": "<e.g. 80000-95000>",
|
| 87 |
+
"estimatedRangeWithAllSkills": "<e.g. 110000-130000>",
|
| 88 |
+
"potentialIncrease": "<e.g. +25000-35000>",
|
| 89 |
+
"topPayingSkills": ["<skill that adds most salary value>", "<next>", "<next>"]
|
| 90 |
+
},
|
| 91 |
+
|
| 92 |
+
"candidateComparison": {
|
| 93 |
+
"strengthsVsTypical": ["<what makes this candidate stand out>"],
|
| 94 |
+
"weaknessesVsTypical": ["<where this candidate falls behind typical applicants>"],
|
| 95 |
+
"competitiveEdge": "<brief summary of how candidate compares to top applicants for this role>"
|
| 96 |
+
},
|
| 97 |
+
|
| 98 |
+
"quickWins": [
|
| 99 |
+
{
|
| 100 |
+
"action": "<specific action to take>",
|
| 101 |
+
"timeRequired": "<e.g. 2 hours, 1 day>",
|
| 102 |
+
"impact": "<high|medium|low>"
|
| 103 |
+
}
|
| 104 |
+
],
|
| 105 |
+
|
| 106 |
+
"learningRoadmap": [
|
| 107 |
+
{
|
| 108 |
+
"week": "<e.g. Week 1-2>",
|
| 109 |
+
"phase": 1,
|
| 110 |
+
"title": "<phase title>",
|
| 111 |
+
"focusSkills": ["<skill>"],
|
| 112 |
+
"dailyHours": 2,
|
| 113 |
+
"milestones": ["<what you should be able to do by end of this phase>"],
|
| 114 |
+
"resources": [
|
| 115 |
+
{
|
| 116 |
+
"name": "<specific course/tutorial/book name>",
|
| 117 |
+
"url": "<actual URL if possible, otherwise empty string>",
|
| 118 |
+
"type": "<course|tutorial|book|project|video|documentation>",
|
| 119 |
+
"platform": "<e.g. freeCodeCamp, Coursera, YouTube, MDN, etc.>",
|
| 120 |
+
"free": true,
|
| 121 |
+
"estimatedHours": 10
|
| 122 |
+
}
|
| 123 |
+
]
|
| 124 |
+
}
|
| 125 |
+
],
|
| 126 |
+
|
| 127 |
+
"portfolioProjects": [
|
| 128 |
+
{
|
| 129 |
+
"title": "<project name>",
|
| 130 |
+
"description": "<what to build>",
|
| 131 |
+
"skillsDemonstrated": ["<skill>"],
|
| 132 |
+
"difficulty": "<beginner|intermediate|advanced>",
|
| 133 |
+
"estimatedTime": "<e.g. 1 week>",
|
| 134 |
+
"techStack": ["<technology>"]
|
| 135 |
+
}
|
| 136 |
+
],
|
| 137 |
+
|
| 138 |
+
"resumeTips": ["<actionable tip to improve resume for this specific role>"],
|
| 139 |
+
|
| 140 |
+
"interviewPrep": [
|
| 141 |
+
"<likely interview question or topic for this role>"
|
| 142 |
+
]
|
| 143 |
}
|
| 144 |
|
| 145 |
+
IMPORTANT INSTRUCTIONS:
|
| 146 |
+
- Be extremely specific with resource recommendations - use real course names, real platforms
|
| 147 |
+
- For salary figures, use realistic market data for this role
|
| 148 |
+
- Priority scores should reflect how critical each skill is for getting hired
|
| 149 |
+
- Quick wins should be things achievable in under a week
|
| 150 |
+
- The learning roadmap should be week-by-week for 4-8 weeks
|
| 151 |
+
- Portfolio projects should be impressive enough to discuss in interviews
|
| 152 |
+
- Compare against what a strong candidate for this specific role looks like
|
| 153 |
+
- Return ONLY the JSON object, no other text`);
|
| 154 |
+
|
| 155 |
return parseJSON(text);
|
| 156 |
}
|
| 157 |
|
frontend/src/components/Results.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React from
|
| 2 |
|
| 3 |
function scoreClass(score) {
|
| 4 |
if (score >= 70) return 'good';
|
|
@@ -21,9 +21,9 @@ export default function Results({ data, onReset }) {
|
|
| 21 |
{/* Matched Skills */}
|
| 22 |
<div className="card">
|
| 23 |
<h3>Skills You Have</h3>
|
| 24 |
-
{a.matchedSkills.map((s, i) => (
|
| 25 |
<span key={i} className={`skill-tag ${s.level}`}>
|
| 26 |
-
{s.skill}
|
| 27 |
</span>
|
| 28 |
))}
|
| 29 |
</div>
|
|
@@ -31,11 +31,13 @@ export default function Results({ data, onReset }) {
|
|
| 31 |
{/* Missing Skills */}
|
| 32 |
<div className="card">
|
| 33 |
<h3>Skills to Develop</h3>
|
| 34 |
-
{a.missingSkills.map((s, i) => (
|
| 35 |
<div key={i} className="missing-skill">
|
| 36 |
<div>
|
| 37 |
-
<div className="name">{s.skill}</div>
|
| 38 |
-
<div className="meta">{s.difficulty} to learn · ~{s.timeToLearn}</div>
|
|
|
|
|
|
|
| 39 |
</div>
|
| 40 |
<span className={`importance-badge ${s.importance}`}>{s.importance}</span>
|
| 41 |
</div>
|
|
@@ -45,14 +47,14 @@ export default function Results({ data, onReset }) {
|
|
| 45 |
{/* Learning Path */}
|
| 46 |
<div className="card">
|
| 47 |
<h3>Learning Path</h3>
|
| 48 |
-
{a.learningPath.map((phase, i) => (
|
| 49 |
<div key={i} className="phase-card">
|
| 50 |
<h4>Phase {phase.phase}: {phase.title}</h4>
|
| 51 |
-
<div className="duration">{phase.duration}</div>
|
| 52 |
-
<div className="skills-list">Skills: {phase.skills.join(', ')}</div>
|
| 53 |
-
{phase.resources.map((r, j) => (
|
| 54 |
<div key={j} className="resource">
|
| 55 |
-
<span className="type">[{r.type}]</span> {r.name} {r.free && <span className="free-badge">FREE</span>}
|
| 56 |
</div>
|
| 57 |
))}
|
| 58 |
</div>
|
|
@@ -62,15 +64,61 @@ export default function Results({ data, onReset }) {
|
|
| 62 |
{/* Quick Wins */}
|
| 63 |
<div className="card list-card wins">
|
| 64 |
<h3>Quick Wins (Do This Week)</h3>
|
| 65 |
-
<ul>{a.quickWins.map((w, i) => <li key={i}>{
|
| 66 |
</div>
|
| 67 |
|
| 68 |
{/* Resume Tips */}
|
| 69 |
<div className="card list-card tips">
|
| 70 |
<h3>Resume Tailoring Tips</h3>
|
| 71 |
-
<ul>{a.resumeTips.map((t, i) => <li key={i}>{t}</li>)}</ul>
|
| 72 |
</div>
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
<button className="btn-secondary" onClick={onReset}>Analyze Another Job</button>
|
| 75 |
</div>
|
| 76 |
);
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
|
| 3 |
function scoreClass(score) {
|
| 4 |
if (score >= 70) return 'good';
|
|
|
|
| 21 |
{/* Matched Skills */}
|
| 22 |
<div className="card">
|
| 23 |
<h3>Skills You Have</h3>
|
| 24 |
+
{(a.matchedSkills||[]).map((s, i) => (
|
| 25 |
<span key={i} className={`skill-tag ${s.level}`}>
|
| 26 |
+
{s.skill}{s.currentProficiency?" ("+s.currentProficiency+")":""}
|
| 27 |
</span>
|
| 28 |
))}
|
| 29 |
</div>
|
|
|
|
| 31 |
{/* Missing Skills */}
|
| 32 |
<div className="card">
|
| 33 |
<h3>Skills to Develop</h3>
|
| 34 |
+
{(a.missingSkills||[]).map((s, i) => (
|
| 35 |
<div key={i} className="missing-skill">
|
| 36 |
<div>
|
| 37 |
+
<div className="name">{s.skill} {s.priorityScore && <span className="priority-score">P{s.priorityScore}/10</span>}</div>
|
| 38 |
+
<div className="meta">{s.difficulty} to learn · ~{s.timeToLearn}{s.industryDemand && <span className="demand-tag"> · {s.industryDemand} demand{s.demandTrend==="rising"?" ↑":s.demandTrend==="declining"?" ↓":""}</span>}</div>
|
| 39 |
+
{s.salaryImpact && <div className="salary-inline">Salary impact: {s.salaryImpact}</div>}
|
| 40 |
+
{s.portfolioProject && <div className="portfolio-inline">Project: {s.portfolioProject}</div>}
|
| 41 |
</div>
|
| 42 |
<span className={`importance-badge ${s.importance}`}>{s.importance}</span>
|
| 43 |
</div>
|
|
|
|
| 47 |
{/* Learning Path */}
|
| 48 |
<div className="card">
|
| 49 |
<h3>Learning Path</h3>
|
| 50 |
+
{(a.learningRoadmap||a.learningPath||[]).map((phase, i) => (
|
| 51 |
<div key={i} className="phase-card">
|
| 52 |
<h4>Phase {phase.phase}: {phase.title}</h4>
|
| 53 |
+
<div className="duration">{phase.week||phase.duration}</div>
|
| 54 |
+
<div className="skills-list">Skills: {(phase.focusSkills||phase.skills||[]).join(', ')}</div>
|
| 55 |
+
{(phase.resources||[]).map((r, j) => (
|
| 56 |
<div key={j} className="resource">
|
| 57 |
+
<span className="type">[{r.type}]</span> {r.url ? <a href={r.url} target="_blank" rel="noreferrer" style={{color:"var(--accent)"}}>{r.name}</a> : r.name}{r.platform ? " ("+r.platform+")" : ""} {r.free && <span className="free-badge">FREE</span>}
|
| 58 |
</div>
|
| 59 |
))}
|
| 60 |
</div>
|
|
|
|
| 64 |
{/* Quick Wins */}
|
| 65 |
<div className="card list-card wins">
|
| 66 |
<h3>Quick Wins (Do This Week)</h3>
|
| 67 |
+
<ul>{(a.quickWins||[]).map((w, i) => { const item = typeof w==="string"?{action:w}:w; return <li key={i}>{item.action}{item.timeRequired?" ("+item.timeRequired+")":""}{item.impact?" - "+item.impact+" impact":""}</li>; })}</ul>
|
| 68 |
</div>
|
| 69 |
|
| 70 |
{/* Resume Tips */}
|
| 71 |
<div className="card list-card tips">
|
| 72 |
<h3>Resume Tailoring Tips</h3>
|
| 73 |
+
<ul>{(a.resumeTips||[]).map((t, i) => <li key={i}>{t}</li>)}</ul>
|
| 74 |
</div>
|
| 75 |
|
| 76 |
+
{/* Candidate Comparison */}
|
| 77 |
+
{a.candidateComparison && (
|
| 78 |
+
<div className="card comparison-card">
|
| 79 |
+
<h3>How You Compare</h3>
|
| 80 |
+
<p className="comp-edge">{a.candidateComparison.competitiveEdge}</p>
|
| 81 |
+
<div className="comp-grid">
|
| 82 |
+
<div className="comp-col good">
|
| 83 |
+
<h4>Your Strengths</h4>
|
| 84 |
+
<ul>{(a.candidateComparison.strengthsVsTypical||[]).map((s,i)=><li key={i}>{s}</li>)}</ul>
|
| 85 |
+
</div>
|
| 86 |
+
<div className="comp-col warn">
|
| 87 |
+
<h4>Areas to Improve</h4>
|
| 88 |
+
<ul>{(a.candidateComparison.weaknessesVsTypical||[]).map((w,i)=><li key={i}>{w}</li>)}</ul>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
)}
|
| 93 |
+
|
| 94 |
+
{/* Salary Analysis */}
|
| 95 |
+
{a.salaryAnalysis && (
|
| 96 |
+
<div className="card salary-card">
|
| 97 |
+
<h3>Salary Impact Analysis</h3>
|
| 98 |
+
<div className="salary-grid">
|
| 99 |
+
<div className="salary-box"><div className="sal-label">Current Skills</div><div className="sal-value">{a.salaryAnalysis.estimatedRangeWithCurrentSkills}</div></div>
|
| 100 |
+
<div className="salary-arrow">{"→"}</div>
|
| 101 |
+
<div className="salary-box target"><div className="sal-label">With All Skills</div><div className="sal-value">{a.salaryAnalysis.estimatedRangeWithAllSkills}</div></div>
|
| 102 |
+
</div>
|
| 103 |
+
<div className="sal-increase">Potential increase: <strong>{a.salaryAnalysis.potentialIncrease}</strong></div>
|
| 104 |
+
{a.salaryAnalysis.topPayingSkills && <div className="top-skills"><strong>Highest-value skills:</strong> {a.salaryAnalysis.topPayingSkills.join(", ")}</div>}
|
| 105 |
+
</div>
|
| 106 |
+
)}
|
| 107 |
+
|
| 108 |
+
{a.portfolioProjects&&a.portfolioProjects.length>0&&(
|
| 109 |
+
<div className={"card"}>
|
| 110 |
+
<h3>Portfolio Projects</h3>
|
| 111 |
+
{a.portfolioProjects.map((p,i)=><div key={i} className={"project-item"}><strong>{p.title}</strong><p>{p.description}</p><div>{p.estimatedTime} | {p.difficulty}</div></div>)}
|
| 112 |
+
</div>
|
| 113 |
+
)}
|
| 114 |
+
|
| 115 |
+
{a.interviewPrep&&a.interviewPrep.length>0&&(
|
| 116 |
+
<div className={"card list-card tips"}>
|
| 117 |
+
<h3>Interview Preparation</h3>
|
| 118 |
+
<ul>{a.interviewPrep.map((q,i)=><li key={i}>{q}</li>)}</ul>
|
| 119 |
+
</div>
|
| 120 |
+
)}
|
| 121 |
+
|
| 122 |
<button className="btn-secondary" onClick={onReset}>Analyze Another Job</button>
|
| 123 |
</div>
|
| 124 |
);
|
frontend/src/styles/global.css
CHANGED
|
@@ -183,3 +183,34 @@ body {
|
|
| 183 |
@keyframes spin { to { transform: rotate(360deg); } }
|
| 184 |
|
| 185 |
@media (max-width: 600px) { .header h1 { font-size: 1.6rem; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
@keyframes spin { to { transform: rotate(360deg); } }
|
| 184 |
|
| 185 |
@media (max-width: 600px) { .header h1 { font-size: 1.6rem; } }
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
.priority-score { font-size:0.75rem; background:rgba(88,166,255,0.15); color:var(--accent); padding:0.1rem 0.4rem; border-radius:8px; margin-left:0.5rem; }
|
| 189 |
+
.demand-tag { color:var(--accent2); font-weight:500; }
|
| 190 |
+
.salary-inline { font-size:0.8rem; color:var(--accent2); margin-top:0.3rem; }
|
| 191 |
+
.portfolio-inline { font-size:0.8rem; color:var(--text-dim); margin-top:0.2rem; font-style:italic; }
|
| 192 |
+
.salary-card { }
|
| 193 |
+
.salary-grid { display:flex; align-items:center; justify-content:center; gap:1.5rem; margin:1rem 0; flex-wrap:wrap; }
|
| 194 |
+
.salary-box { background:var(--bg); padding:1.2rem 1.5rem; border-radius:10px; text-align:center; min-width:180px; }
|
| 195 |
+
.salary-box.target { border:2px solid var(--accent2); }
|
| 196 |
+
.sal-label { font-size:0.8rem; color:var(--text-dim); margin-bottom:0.3rem; }
|
| 197 |
+
.sal-value { font-size:1.4rem; font-weight:700; color:var(--text); }
|
| 198 |
+
.salary-arrow { font-size:2rem; color:var(--accent); }
|
| 199 |
+
.sal-increase { text-align:center; margin-top:1rem; font-size:1.1rem; color:var(--accent2); }
|
| 200 |
+
.top-skills { text-align:center; margin-top:0.8rem; color:var(--text-dim); font-size:0.9rem; }
|
| 201 |
+
.comparison-card { }
|
| 202 |
+
.comp-edge { color:var(--text-dim); font-size:0.95rem; margin-bottom:1rem; line-height:1.5; }
|
| 203 |
+
.comp-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
|
| 204 |
+
.comp-col { background:var(--bg); padding:1rem; border-radius:8px; }
|
| 205 |
+
.comp-col h4 { margin-bottom:0.5rem; font-size:0.9rem; }
|
| 206 |
+
.comp-col.good { border-left:3px solid var(--accent2); }
|
| 207 |
+
.comp-col.good h4 { color:var(--accent2); }
|
| 208 |
+
.comp-col.warn { border-left:3px solid var(--warning); }
|
| 209 |
+
.comp-col.warn h4 { color:var(--warning); }
|
| 210 |
+
.comp-col ul { list-style:none; padding:0; }
|
| 211 |
+
.comp-col li { font-size:0.85rem; color:var(--text-dim); padding:0.3rem 0; }
|
| 212 |
+
.project-item { background:var(--bg); padding:1rem; border-radius:8px; margin-bottom:0.8rem; }
|
| 213 |
+
.project-item strong { color:var(--accent); }
|
| 214 |
+
.project-item p { color:var(--text-dim); font-size:0.9rem; margin:0.4rem 0; }
|
| 215 |
+
.proj-meta { font-size:0.8rem; color:var(--text-dim); }
|
| 216 |
+
@media(max-width:600px){.comp-grid{grid-template-columns:1fr;}.salary-grid{flex-direction:column;}}
|