Page parcours : logos entreprises, badges de type colorés, durées calculées, support light/dark mode (FR/EN/AR)

This commit is contained in:
Jalil Arfaoui 2026-03-11 15:12:53 +01:00
parent 9871f189e3
commit 8728594347
36 changed files with 488 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -59,6 +59,7 @@ const experiencesCollection = defineCollection({
role: z.string(), role: z.string(),
company: z.string(), company: z.string(),
companyUrl: z.string().url().optional(), companyUrl: z.string().url().optional(),
logo: z.string().optional(),
location: z.string().optional(), location: z.string().optional(),
startDate: z.string(), startDate: z.string(),
endDate: z.string().optional(), endDate: z.string().optional(),

View file

@ -2,6 +2,7 @@
role: "مهندس معماري للواجهات" role: "مهندس معماري للواجهات"
company: "ARaymond" company: "ARaymond"
companyUrl: "https://www.araymond.com/" companyUrl: "https://www.araymond.com/"
logo: "araymond.png"
location: "غرونوبل" location: "غرونوبل"
startDate: "2022-01" startDate: "2022-01"
endDate: "2023-01" endDate: "2023-01"

View file

@ -2,6 +2,7 @@
role: "Frontend Architect" role: "Frontend Architect"
company: "ARaymond" company: "ARaymond"
companyUrl: "https://www.araymond.com/" companyUrl: "https://www.araymond.com/"
logo: "araymond.png"
location: "Grenoble" location: "Grenoble"
startDate: "2022-01" startDate: "2022-01"
endDate: "2023-01" endDate: "2023-01"

View file

@ -2,6 +2,7 @@
role: "Architecte frontend" role: "Architecte frontend"
company: "ARaymond" company: "ARaymond"
companyUrl: "https://www.araymond.com/" companyUrl: "https://www.araymond.com/"
logo: "araymond.png"
location: "Grenoble" location: "Grenoble"
startDate: "2022-01" startDate: "2022-01"
endDate: "2023-01" endDate: "2023-01"

View file

@ -2,6 +2,7 @@
role: "أستاذ هندسة البرمجيات" role: "أستاذ هندسة البرمجيات"
company: "جامعة شامبوليون" company: "جامعة شامبوليون"
companyUrl: "https://www.univ-jfc.fr/" companyUrl: "https://www.univ-jfc.fr/"
logo: "champollion.png"
location: "ألبي" location: "ألبي"
startDate: "2019-09" startDate: "2019-09"
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"] technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]

View file

@ -2,6 +2,7 @@
role: "Software Engineering Professor" role: "Software Engineering Professor"
company: "Université Champollion" company: "Université Champollion"
companyUrl: "https://www.univ-jfc.fr/" companyUrl: "https://www.univ-jfc.fr/"
logo: "champollion.png"
location: "Albi" location: "Albi"
startDate: "2019-09" startDate: "2019-09"
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"] technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]

View file

@ -2,6 +2,7 @@
role: "Enseignant en génie logiciel" role: "Enseignant en génie logiciel"
company: "Université Champollion" company: "Université Champollion"
companyUrl: "https://www.univ-jfc.fr/" companyUrl: "https://www.univ-jfc.fr/"
logo: "champollion.png"
location: "Albi" location: "Albi"
startDate: "2019-09" startDate: "2019-09"
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"] technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]

View file

@ -2,6 +2,7 @@
role: "حرفي برمجيات / مؤسس مشارك" role: "حرفي برمجيات / مؤسس مشارك"
company: "DisMoi" company: "DisMoi"
companyUrl: "https://github.com/dis-moi" companyUrl: "https://github.com/dis-moi"
logo: "dismoi.png"
location: "عن بُعد" location: "عن بُعد"
startDate: "2019-01" startDate: "2019-01"
endDate: "2021-06" endDate: "2021-06"

View file

@ -2,6 +2,7 @@
role: "Software Craftsman / Cofounder" role: "Software Craftsman / Cofounder"
company: "DisMoi" company: "DisMoi"
companyUrl: "https://github.com/dis-moi" companyUrl: "https://github.com/dis-moi"
logo: "dismoi.png"
location: "Remote" location: "Remote"
startDate: "2019-01" startDate: "2019-01"
endDate: "2021-06" endDate: "2021-06"

View file

@ -2,6 +2,7 @@
role: "Software Craftsman / Cofondateur" role: "Software Craftsman / Cofondateur"
company: "DisMoi" company: "DisMoi"
companyUrl: "https://github.com/dis-moi" companyUrl: "https://github.com/dis-moi"
logo: "dismoi.png"
location: "Remote" location: "Remote"
startDate: "2019-01" startDate: "2019-01"
endDate: "2021-06" endDate: "2021-06"

View file

@ -2,6 +2,7 @@
role: "مهندس برمجيات fullstack" role: "مهندس برمجيات fullstack"
company: "Libeo" company: "Libeo"
companyUrl: "https://www.libeo.io/" companyUrl: "https://www.libeo.io/"
logo: "libeo.png"
location: "عن بُعد" location: "عن بُعد"
startDate: "2021-01" startDate: "2021-01"
endDate: "2021-06" endDate: "2021-06"

View file

@ -2,6 +2,7 @@
role: "Fullstack Software Engineer" role: "Fullstack Software Engineer"
company: "Libeo" company: "Libeo"
companyUrl: "https://www.libeo.io/" companyUrl: "https://www.libeo.io/"
logo: "libeo.png"
location: "Remote" location: "Remote"
startDate: "2021-01" startDate: "2021-01"
endDate: "2021-06" endDate: "2021-06"

View file

@ -2,6 +2,7 @@
role: "Ingénieur logiciel fullstack" role: "Ingénieur logiciel fullstack"
company: "Libeo" company: "Libeo"
companyUrl: "https://www.libeo.io/" companyUrl: "https://www.libeo.io/"
logo: "libeo.png"
location: "Remote" location: "Remote"
startDate: "2021-01" startDate: "2021-01"
endDate: "2021-06" endDate: "2021-06"

View file

@ -2,6 +2,7 @@
role: "مهندس برمجيات أول" role: "مهندس برمجيات أول"
company: "Obat" company: "Obat"
companyUrl: "https://www.obat.fr/" companyUrl: "https://www.obat.fr/"
logo: "obat.png"
location: "عن بُعد" location: "عن بُعد"
startDate: "2023-02" startDate: "2023-02"
endDate: "2024-01" endDate: "2024-01"

View file

@ -2,6 +2,7 @@
role: "Senior Software Engineer" role: "Senior Software Engineer"
company: "Obat" company: "Obat"
companyUrl: "https://www.obat.fr/" companyUrl: "https://www.obat.fr/"
logo: "obat.png"
location: "Remote" location: "Remote"
startDate: "2023-02" startDate: "2023-02"
endDate: "2024-01" endDate: "2024-01"

View file

@ -2,6 +2,7 @@
role: "Ingénieur logiciel senior" role: "Ingénieur logiciel senior"
company: "Obat" company: "Obat"
companyUrl: "https://www.obat.fr/" companyUrl: "https://www.obat.fr/"
logo: "obat.png"
location: "Remote" location: "Remote"
startDate: "2023-02" startDate: "2023-02"
endDate: "2024-01" endDate: "2024-01"

View file

@ -2,6 +2,7 @@
role: "مطوّر رئيسي" role: "مطوّر رئيسي"
company: "Urssaf Caisse nationale" company: "Urssaf Caisse nationale"
companyUrl: "https://www.urssaf.fr/" companyUrl: "https://www.urssaf.fr/"
logo: "urssaf.png"
location: "عن بُعد / باريس" location: "عن بُعد / باريس"
startDate: "2024-02" startDate: "2024-02"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"] technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]

View file

@ -2,6 +2,7 @@
role: "Lead Developer" role: "Lead Developer"
company: "Urssaf Caisse nationale" company: "Urssaf Caisse nationale"
companyUrl: "https://www.urssaf.fr/" companyUrl: "https://www.urssaf.fr/"
logo: "urssaf.png"
location: "Remote / Paris" location: "Remote / Paris"
startDate: "2024-02" startDate: "2024-02"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"] technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]

View file

@ -2,6 +2,7 @@
role: "Lead Developer" role: "Lead Developer"
company: "Urssaf Caisse nationale" company: "Urssaf Caisse nationale"
companyUrl: "https://www.urssaf.fr/" companyUrl: "https://www.urssaf.fr/"
logo: "urssaf.png"
location: "Remote / Paris" location: "Remote / Paris"
startDate: "2024-02" startDate: "2024-02"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"] technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]

View file

@ -2,6 +2,7 @@
role: "مطوّر واجهات رئيسي Travel" role: "مطوّر واجهات رئيسي Travel"
company: "Veepee" company: "Veepee"
companyUrl: "https://www.veepee.com/" companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "باريس" location: "باريس"
startDate: "2016-02" startDate: "2016-02"
endDate: "2018-01" endDate: "2018-01"

View file

@ -2,6 +2,7 @@
role: "Travel Front Lead Developer" role: "Travel Front Lead Developer"
company: "Veepee" company: "Veepee"
companyUrl: "https://www.veepee.com/" companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "Paris" location: "Paris"
startDate: "2016-02" startDate: "2016-02"
endDate: "2018-01" endDate: "2018-01"

View file

@ -2,6 +2,7 @@
role: "Travel Front Lead Developer" role: "Travel Front Lead Developer"
company: "Veepee" company: "Veepee"
companyUrl: "https://www.veepee.com/" companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "Paris" location: "Paris"
startDate: "2016-02" startDate: "2016-02"
endDate: "2018-01" endDate: "2018-01"

View file

@ -2,6 +2,7 @@
role: "قائد تقني Travel" role: "قائد تقني Travel"
company: "Veepee" company: "Veepee"
companyUrl: "https://www.veepee.com/" companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "باريس" location: "باريس"
startDate: "2018-01" startDate: "2018-01"
endDate: "2019-06" endDate: "2019-06"

View file

@ -2,6 +2,7 @@
role: "Travel Tech Lead" role: "Travel Tech Lead"
company: "Veepee" company: "Veepee"
companyUrl: "https://www.veepee.com/" companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "Paris" location: "Paris"
startDate: "2018-01" startDate: "2018-01"
endDate: "2019-06" endDate: "2019-06"

View file

@ -2,6 +2,7 @@
role: "Travel Tech Lead" role: "Travel Tech Lead"
company: "Veepee" company: "Veepee"
companyUrl: "https://www.veepee.com/" companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "Paris" location: "Paris"
startDate: "2018-01" startDate: "2018-01"
endDate: "2019-06" endDate: "2019-06"

View file

@ -57,8 +57,8 @@ const locale = pathname.startsWith("/en")
[data-facet="code"] > section h1, [data-facet="code"] > section h1,
[data-facet="code"] > section h2, [data-facet="code"] > section h2,
[data-facet="code"] > section h3, [data-facet="code"] > section h3:not(.exp-card h3):not(.facet-card h3),
[data-facet="code"] > section strong { [data-facet="code"] > section strong:not(.exp-card strong):not(.facet-card strong) {
color: white !important; color: white !important;
} }
@ -72,8 +72,8 @@ const locale = pathname.startsWith("/en")
text-decoration-color: white !important; text-decoration-color: white !important;
} }
[data-facet="code"] > section > :not(.facet-card) p, [data-facet="code"] > section p:not(.exp-card p):not(.facet-card p),
[data-facet="code"] > section > :not(.facet-card) li { [data-facet="code"] > section li:not(.exp-card li):not(.facet-card li) {
color: rgba(255, 255, 255, 0.75) !important; color: rgba(255, 255, 255, 0.75) !important;
} }

View file

@ -1,16 +1,37 @@
--- ---
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import { Image } from "astro:assets";
import Layout from "../../../layouts/main.astro"; import Layout from "../../../layouts/main.astro";
const logos = import.meta.glob<{ default: ImageMetadata }>('../../../assets/images/companies/*.png', { eager: true });
function getLogo(filename?: string) {
if (!filename) return null;
const key = `../../../assets/images/companies/${filename}`;
return logos[key]?.default ?? null;
}
function getInitials(company: string) {
return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
}
const locale = "ar"; const locale = "ar";
const experiences = (await getCollection("experiences")) const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft) .filter((e) => e.data.lang === locale && !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const typeIcons: Record<string, string> = { const typeLabels: Record<string, string> = {
employment: '🏢', freelance: '💼', teaching: '🎓', employment: 'وظيفة', freelance: 'مستقل', teaching: 'تدريس',
community: '🤝', entrepreneurship: '🚀', community: 'مجتمع', entrepreneurship: 'ريادة أعمال',
};
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
}; };
function formatMonth(dateStr: string) { function formatMonth(dateStr: string) {
@ -18,6 +39,17 @@ function formatMonth(dateStr: string) {
return new Date(parseInt(year), parseInt(month) - 1) return new Date(parseInt(year), parseInt(month) - 1)
.toLocaleDateString('ar-SA', { year: 'numeric', month: 'short' }); .toLocaleDateString('ar-SA', { year: 'numeric', month: 'short' });
} }
function computeDuration(startStr: string, endStr?: string) {
const [sy, sm] = startStr.split('-').map(Number);
const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
if (years === 0) return `${months} أشهر`;
if (months === 0) return `${years} سنة`;
return `${years} سنة ${months} أشهر`;
}
--- ---
<Layout <Layout
@ -43,43 +75,68 @@ function formatMonth(dateStr: string) {
const isOngoing = !exp.data.endDate; const isOngoing = !exp.data.endDate;
const start = formatMonth(exp.data.startDate); const start = formatMonth(exp.data.startDate);
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'الحاضر'; const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'الحاضر';
const duration = computeDuration(exp.data.startDate, exp.data.endDate);
const colors = typeColors[exp.data.type] || typeColors.employment;
const label = typeLabels[exp.data.type] || exp.data.type;
return ( return (
<div class="relative pr-8 pb-10 border-r-2 border-white/[0.1] last:pb-0"> <div class="relative pr-10 pb-10 border-r-2 border-white/[0.12] last:pb-0">
<div <div
class:list={[ class:list={[
"absolute -right-[9px] top-1 w-4 h-4 rounded-full border-2", "absolute -right-[7px] top-2 w-3 h-3 rounded-full border-2",
isOngoing isOngoing ? colors.dot : "border-white/20 bg-white/10"
? "border-purple-300 bg-purple-400"
: "border-white/20 bg-white/10"
]} ]}
> >
{isOngoing && <div class="absolute inset-0.5 rounded-full bg-purple-300 animate-pulse" />} {isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
</div> </div>
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-5 hover:bg-white/[0.08] transition-colors duration-200"> <div class:list={[
<div class="mb-2 flex items-center gap-2 flex-wrap"> "exp-card relative rounded-2xl p-6 transition-colors duration-200 border-r-[3px]",
<span class="text-xs text-white/40"> isOngoing ? "exp-card--ongoing" : "",
{typeIcons[exp.data.type] || '💼'} {start} — {end} colors.border
]}>
{getLogo(exp.data.logo) ? (
<div class="exp-logo absolute top-5 left-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
<Image
src={getLogo(exp.data.logo)!}
alt={`شعار ${exp.data.company}`}
width={52}
height={52}
class="rounded-lg object-contain"
/>
</div>
) : (
<div class="exp-logo-fallback absolute top-5 left-5 w-16 h-16 rounded-xl flex items-center justify-center">
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
</div>
)}
<div class="mb-3 flex items-center gap-2 flex-wrap pl-20">
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
{label}
</span> </span>
{exp.data.location && <span class="text-xs text-white/30">· {exp.data.location}</span>} <span class="exp-meta text-xs">
{start} — {end}
</span>
<span class="exp-meta text-xs">· {duration}</span>
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
</div> </div>
<h3 class="text-lg font-bold text-white">{exp.data.role}</h3> <h3 class="text-xl font-bold pl-20">{exp.data.role}</h3>
<p class="text-sm font-medium text-purple-200 mb-3"> <p class="exp-company text-sm font-medium mt-1 mb-3">
{exp.data.companyUrl ? ( {exp.data.companyUrl ? (
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">{exp.data.company}</a> <a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
) : exp.data.company} ) : exp.data.company}
</p> </p>
<div class="text-sm text-white/60 leading-relaxed mb-3 prose prose-sm prose-invert max-w-none [&_p]:text-white/60 [&_a]:text-purple-200 [&_a:hover]:text-white"> <div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
<Content /> <Content />
</div> </div>
{exp.data.technologies && exp.data.technologies.length > 0 && ( {exp.data.technologies && exp.data.technologies.length > 0 && (
<div class="flex flex-wrap gap-1.5"> <div class="flex flex-wrap gap-1.5">
{exp.data.technologies.map((tech: string) => ( {exp.data.technologies.map((tech: string) => (
<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/[0.06] text-white/50 border border-white/[0.06]"> <span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
{tech} {tech}
</span> </span>
))} ))}
@ -92,3 +149,80 @@ function formatMonth(dateStr: string) {
</div> </div>
</section> </section>
</Layout> </Layout>
<style>
/* ── Light mode (default): white cards on purple gradient ── */
.exp-card {
background: white;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.exp-card--ongoing { box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.25), 0 1px 3px rgba(0, 0, 0, 0.08); }
.exp-card h3 { color: #111827 !important; }
.exp-card .exp-meta { color: #9ca3af; }
.exp-card .exp-company { color: #6b21a8; }
.exp-card .exp-company a { color: #6b21a8 !important; text-decoration: none !important; }
.exp-card .exp-company a:hover { color: #581c87 !important; }
.exp-card .exp-content,
.exp-card .exp-content p { color: #4b5563 !important; }
.exp-card .exp-content a { color: #6b21a8 !important; text-decoration-color: rgba(107, 33, 168, 0.3) !important; }
.exp-card .exp-content a:hover { color: #581c87 !important; }
.exp-card .exp-tech { background: #f3f4f6; color: #6b7280; border-color: #e5e7eb; }
.exp-logo { background: #f9fafb; border: 1px solid #e5e7eb; }
.exp-logo-fallback { background: #f3f4f6; border: 1px solid #e5e7eb; }
.exp-logo-fallback span { color: #9ca3af; }
/* Type badges light */
.type-badge-blue { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
.type-badge-amber { background: #fffbeb; color: #b45309; border-color: #fde68a; }
.type-badge-emerald { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
.type-badge-pink { background: #fdf2f8; color: #be185d; border-color: #fbcfe8; }
.type-badge-purple { background: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
/* Type borders light (RTL: border-right) */
.type-border-blue { border-right-color: #3b82f6 !important; }
.type-border-amber { border-right-color: #f59e0b !important; }
.type-border-emerald { border-right-color: #10b981 !important; }
.type-border-pink { border-right-color: #ec4899 !important; }
.type-border-purple { border-right-color: #a855f7 !important; }
/* ── Dark mode: translucent cards ── */
:global(.dark) .exp-card {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
:global(.dark) .exp-card:hover { background: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-card--ongoing {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.12);
box-shadow: none;
}
:global(.dark) .exp-card h3 { color: white !important; }
:global(.dark) .exp-card .exp-meta { color: rgba(255, 255, 255, 0.3); }
:global(.dark) .exp-card .exp-company { color: #d4b5ff; }
:global(.dark) .exp-card .exp-company a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-company a:hover { color: white !important; }
:global(.dark) .exp-card .exp-content,
:global(.dark) .exp-card .exp-content p { color: rgba(255, 255, 255, 0.6) !important; }
:global(.dark) .exp-card .exp-content a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-content a:hover { color: white !important; }
:global(.dark) .exp-card .exp-tech { background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.45); border-color: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-logo { background: white; border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); }
:global(.dark) .exp-logo-fallback { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.1); }
:global(.dark) .exp-logo-fallback span { color: rgba(255, 255, 255, 0.4); }
/* Type badges dark */
:global(.dark) .type-badge-blue { background: rgba(96, 165, 250, 0.15); color: #93c5fd; border-color: rgba(96, 165, 250, 0.2); }
:global(.dark) .type-badge-amber { background: rgba(251, 191, 36, 0.15); color: #fcd34d; border-color: rgba(251, 191, 36, 0.2); }
:global(.dark) .type-badge-emerald { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.2); }
:global(.dark) .type-badge-pink { background: rgba(244, 114, 182, 0.15); color: #f9a8d4; border-color: rgba(244, 114, 182, 0.2); }
:global(.dark) .type-badge-purple { background: rgba(168, 85, 247, 0.15); color: #d4b5ff; border-color: rgba(168, 85, 247, 0.2); }
/* Type borders dark (RTL: border-right) */
:global(.dark) .type-border-blue { border-right-color: rgba(96, 165, 250, 0.4) !important; }
:global(.dark) .type-border-amber { border-right-color: rgba(251, 191, 36, 0.4) !important; }
:global(.dark) .type-border-emerald { border-right-color: rgba(52, 211, 153, 0.4) !important; }
:global(.dark) .type-border-pink { border-right-color: rgba(244, 114, 182, 0.4) !important; }
:global(.dark) .type-border-purple { border-right-color: rgba(168, 85, 247, 0.4) !important; }
</style>

View file

@ -1,16 +1,37 @@
--- ---
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import { Image } from "astro:assets";
import Layout from "../../layouts/main.astro"; import Layout from "../../layouts/main.astro";
const logos = import.meta.glob<{ default: ImageMetadata }>('../../assets/images/companies/*.png', { eager: true });
function getLogo(filename?: string) {
if (!filename) return null;
const key = `../../assets/images/companies/${filename}`;
return logos[key]?.default ?? null;
}
function getInitials(company: string) {
return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
}
const locale = "fr"; const locale = "fr";
const experiences = (await getCollection("experiences")) const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft) .filter((e) => e.data.lang === locale && !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const typeIcons: Record<string, string> = { const typeLabels: Record<string, string> = {
employment: '🏢', freelance: '💼', teaching: '🎓', employment: 'Emploi', freelance: 'Freelance', teaching: 'Enseignement',
community: '🤝', entrepreneurship: '🚀', community: 'Communauté', entrepreneurship: 'Entrepreneuriat',
};
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
}; };
function formatMonth(dateStr: string) { function formatMonth(dateStr: string) {
@ -18,6 +39,17 @@ function formatMonth(dateStr: string) {
return new Date(parseInt(year), parseInt(month) - 1) return new Date(parseInt(year), parseInt(month) - 1)
.toLocaleDateString('fr-FR', { year: 'numeric', month: 'short' }); .toLocaleDateString('fr-FR', { year: 'numeric', month: 'short' });
} }
function computeDuration(startStr: string, endStr?: string) {
const [sy, sm] = startStr.split('-').map(Number);
const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
if (years === 0) return `${months} mois`;
if (months === 0) return `${years} an${years > 1 ? 's' : ''}`;
return `${years} an${years > 1 ? 's' : ''} ${months} mois`;
}
--- ---
<Layout <Layout
@ -43,43 +75,68 @@ function formatMonth(dateStr: string) {
const isOngoing = !exp.data.endDate; const isOngoing = !exp.data.endDate;
const start = formatMonth(exp.data.startDate); const start = formatMonth(exp.data.startDate);
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent'; const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent';
const duration = computeDuration(exp.data.startDate, exp.data.endDate);
const colors = typeColors[exp.data.type] || typeColors.employment;
const label = typeLabels[exp.data.type] || exp.data.type;
return ( return (
<div class="relative pl-8 pb-10 border-l-2 border-white/[0.1] last:pb-0"> <div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
<div <div
class:list={[ class:list={[
"absolute -left-[9px] top-1 w-4 h-4 rounded-full border-2", "absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
isOngoing isOngoing ? colors.dot : "border-white/20 bg-white/10"
? "border-purple-300 bg-purple-400"
: "border-white/20 bg-white/10"
]} ]}
> >
{isOngoing && <div class="absolute inset-0.5 rounded-full bg-purple-300 animate-pulse" />} {isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
</div> </div>
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-5 hover:bg-white/[0.08] transition-colors duration-200"> <div class:list={[
<div class="mb-2 flex items-center gap-2 flex-wrap"> "exp-card relative rounded-2xl p-6 transition-colors duration-200 border-l-[3px]",
<span class="text-xs text-white/40"> isOngoing ? "exp-card--ongoing" : "",
{typeIcons[exp.data.type] || '💼'} {start} — {end} colors.border
]}>
{getLogo(exp.data.logo) ? (
<div class="exp-logo absolute top-5 right-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
<Image
src={getLogo(exp.data.logo)!}
alt={`Logo ${exp.data.company}`}
width={52}
height={52}
class="rounded-lg object-contain"
/>
</div>
) : (
<div class="exp-logo-fallback absolute top-5 right-5 w-16 h-16 rounded-xl flex items-center justify-center">
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
</div>
)}
<div class="mb-3 flex items-center gap-2 flex-wrap pr-20">
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
{label}
</span> </span>
{exp.data.location && <span class="text-xs text-white/30">· {exp.data.location}</span>} <span class="exp-meta text-xs">
{start} — {end}
</span>
<span class="exp-meta text-xs">· {duration}</span>
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
</div> </div>
<h3 class="text-lg font-bold text-white">{exp.data.role}</h3> <h3 class="text-xl font-bold pr-20">{exp.data.role}</h3>
<p class="text-sm font-medium text-purple-200 mb-3"> <p class="exp-company text-sm font-medium mt-1 mb-3">
{exp.data.companyUrl ? ( {exp.data.companyUrl ? (
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">{exp.data.company}</a> <a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
) : exp.data.company} ) : exp.data.company}
</p> </p>
<div class="text-sm text-white/60 leading-relaxed mb-3 prose prose-sm prose-invert max-w-none [&_p]:text-white/60 [&_a]:text-purple-200 [&_a:hover]:text-white"> <div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
<Content /> <Content />
</div> </div>
{exp.data.technologies && exp.data.technologies.length > 0 && ( {exp.data.technologies && exp.data.technologies.length > 0 && (
<div class="flex flex-wrap gap-1.5"> <div class="flex flex-wrap gap-1.5">
{exp.data.technologies.map((tech: string) => ( {exp.data.technologies.map((tech: string) => (
<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/[0.06] text-white/50 border border-white/[0.06]"> <span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
{tech} {tech}
</span> </span>
))} ))}
@ -92,3 +149,80 @@ function formatMonth(dateStr: string) {
</div> </div>
</section> </section>
</Layout> </Layout>
<style>
/* ── Light mode (default): white cards on purple gradient ── */
.exp-card {
background: white;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.exp-card--ongoing { box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.25), 0 1px 3px rgba(0, 0, 0, 0.08); }
.exp-card h3 { color: #111827 !important; }
.exp-card .exp-meta { color: #9ca3af; }
.exp-card .exp-company { color: #6b21a8; }
.exp-card .exp-company a { color: #6b21a8 !important; text-decoration: none !important; }
.exp-card .exp-company a:hover { color: #581c87 !important; }
.exp-card .exp-content,
.exp-card .exp-content p { color: #4b5563 !important; }
.exp-card .exp-content a { color: #6b21a8 !important; text-decoration-color: rgba(107, 33, 168, 0.3) !important; }
.exp-card .exp-content a:hover { color: #581c87 !important; }
.exp-card .exp-tech { background: #f3f4f6; color: #6b7280; border-color: #e5e7eb; }
.exp-logo { background: #f9fafb; border: 1px solid #e5e7eb; }
.exp-logo-fallback { background: #f3f4f6; border: 1px solid #e5e7eb; }
.exp-logo-fallback span { color: #9ca3af; }
/* Type badges light */
.type-badge-blue { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
.type-badge-amber { background: #fffbeb; color: #b45309; border-color: #fde68a; }
.type-badge-emerald { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
.type-badge-pink { background: #fdf2f8; color: #be185d; border-color: #fbcfe8; }
.type-badge-purple { background: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
/* Type borders light */
.type-border-blue { border-left-color: #3b82f6 !important; }
.type-border-amber { border-left-color: #f59e0b !important; }
.type-border-emerald { border-left-color: #10b981 !important; }
.type-border-pink { border-left-color: #ec4899 !important; }
.type-border-purple { border-left-color: #a855f7 !important; }
/* ── Dark mode: translucent cards ── */
:global(.dark) .exp-card {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
:global(.dark) .exp-card:hover { background: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-card--ongoing {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.12);
box-shadow: none;
}
:global(.dark) .exp-card h3 { color: white !important; }
:global(.dark) .exp-card .exp-meta { color: rgba(255, 255, 255, 0.3); }
:global(.dark) .exp-card .exp-company { color: #d4b5ff; }
:global(.dark) .exp-card .exp-company a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-company a:hover { color: white !important; }
:global(.dark) .exp-card .exp-content,
:global(.dark) .exp-card .exp-content p { color: rgba(255, 255, 255, 0.6) !important; }
:global(.dark) .exp-card .exp-content a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-content a:hover { color: white !important; }
:global(.dark) .exp-card .exp-tech { background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.45); border-color: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-logo { background: white; border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); }
:global(.dark) .exp-logo-fallback { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.1); }
:global(.dark) .exp-logo-fallback span { color: rgba(255, 255, 255, 0.4); }
/* Type badges dark */
:global(.dark) .type-badge-blue { background: rgba(96, 165, 250, 0.15); color: #93c5fd; border-color: rgba(96, 165, 250, 0.2); }
:global(.dark) .type-badge-amber { background: rgba(251, 191, 36, 0.15); color: #fcd34d; border-color: rgba(251, 191, 36, 0.2); }
:global(.dark) .type-badge-emerald { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.2); }
:global(.dark) .type-badge-pink { background: rgba(244, 114, 182, 0.15); color: #f9a8d4; border-color: rgba(244, 114, 182, 0.2); }
:global(.dark) .type-badge-purple { background: rgba(168, 85, 247, 0.15); color: #d4b5ff; border-color: rgba(168, 85, 247, 0.2); }
/* Type borders dark */
:global(.dark) .type-border-blue { border-left-color: rgba(96, 165, 250, 0.4) !important; }
:global(.dark) .type-border-amber { border-left-color: rgba(251, 191, 36, 0.4) !important; }
:global(.dark) .type-border-emerald { border-left-color: rgba(52, 211, 153, 0.4) !important; }
:global(.dark) .type-border-pink { border-left-color: rgba(244, 114, 182, 0.4) !important; }
:global(.dark) .type-border-purple { border-left-color: rgba(168, 85, 247, 0.4) !important; }
</style>

View file

@ -1,16 +1,37 @@
--- ---
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import { Image } from "astro:assets";
import Layout from "../../../layouts/main.astro"; import Layout from "../../../layouts/main.astro";
const logos = import.meta.glob<{ default: ImageMetadata }>('../../../assets/images/companies/*.png', { eager: true });
function getLogo(filename?: string) {
if (!filename) return null;
const key = `../../../assets/images/companies/${filename}`;
return logos[key]?.default ?? null;
}
function getInitials(company: string) {
return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
}
const locale = "en"; const locale = "en";
const experiences = (await getCollection("experiences")) const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft) .filter((e) => e.data.lang === locale && !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const typeIcons: Record<string, string> = { const typeLabels: Record<string, string> = {
employment: '🏢', freelance: '💼', teaching: '🎓', employment: 'Employment', freelance: 'Freelance', teaching: 'Teaching',
community: '🤝', entrepreneurship: '🚀', community: 'Community', entrepreneurship: 'Entrepreneurship',
};
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
}; };
function formatMonth(dateStr: string) { function formatMonth(dateStr: string) {
@ -18,6 +39,17 @@ function formatMonth(dateStr: string) {
return new Date(parseInt(year), parseInt(month) - 1) return new Date(parseInt(year), parseInt(month) - 1)
.toLocaleDateString('en-US', { year: 'numeric', month: 'short' }); .toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
} }
function computeDuration(startStr: string, endStr?: string) {
const [sy, sm] = startStr.split('-').map(Number);
const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
if (years === 0) return `${months} mo`;
if (months === 0) return `${years} yr${years > 1 ? 's' : ''}`;
return `${years} yr${years > 1 ? 's' : ''} ${months} mo`;
}
--- ---
<Layout <Layout
@ -43,43 +75,68 @@ function formatMonth(dateStr: string) {
const isOngoing = !exp.data.endDate; const isOngoing = !exp.data.endDate;
const start = formatMonth(exp.data.startDate); const start = formatMonth(exp.data.startDate);
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Present'; const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Present';
const duration = computeDuration(exp.data.startDate, exp.data.endDate);
const colors = typeColors[exp.data.type] || typeColors.employment;
const label = typeLabels[exp.data.type] || exp.data.type;
return ( return (
<div class="relative pl-8 pb-10 border-l-2 border-white/[0.1] last:pb-0"> <div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
<div <div
class:list={[ class:list={[
"absolute -left-[9px] top-1 w-4 h-4 rounded-full border-2", "absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
isOngoing isOngoing ? colors.dot : "border-white/20 bg-white/10"
? "border-purple-300 bg-purple-400"
: "border-white/20 bg-white/10"
]} ]}
> >
{isOngoing && <div class="absolute inset-0.5 rounded-full bg-purple-300 animate-pulse" />} {isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
</div> </div>
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-5 hover:bg-white/[0.08] transition-colors duration-200"> <div class:list={[
<div class="mb-2 flex items-center gap-2 flex-wrap"> "exp-card relative rounded-2xl p-6 transition-colors duration-200 border-l-[3px]",
<span class="text-xs text-white/40"> isOngoing ? "exp-card--ongoing" : "",
{typeIcons[exp.data.type] || '💼'} {start} — {end} colors.border
]}>
{getLogo(exp.data.logo) ? (
<div class="exp-logo absolute top-5 right-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
<Image
src={getLogo(exp.data.logo)!}
alt={`${exp.data.company} logo`}
width={52}
height={52}
class="rounded-lg object-contain"
/>
</div>
) : (
<div class="exp-logo-fallback absolute top-5 right-5 w-16 h-16 rounded-xl flex items-center justify-center">
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
</div>
)}
<div class="mb-3 flex items-center gap-2 flex-wrap pr-20">
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
{label}
</span> </span>
{exp.data.location && <span class="text-xs text-white/30">· {exp.data.location}</span>} <span class="exp-meta text-xs">
{start} — {end}
</span>
<span class="exp-meta text-xs">· {duration}</span>
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
</div> </div>
<h3 class="text-lg font-bold text-white">{exp.data.role}</h3> <h3 class="text-xl font-bold pr-20">{exp.data.role}</h3>
<p class="text-sm font-medium text-purple-200 mb-3"> <p class="exp-company text-sm font-medium mt-1 mb-3">
{exp.data.companyUrl ? ( {exp.data.companyUrl ? (
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">{exp.data.company}</a> <a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
) : exp.data.company} ) : exp.data.company}
</p> </p>
<div class="text-sm text-white/60 leading-relaxed mb-3 prose prose-sm prose-invert max-w-none [&_p]:text-white/60 [&_a]:text-purple-200 [&_a:hover]:text-white"> <div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
<Content /> <Content />
</div> </div>
{exp.data.technologies && exp.data.technologies.length > 0 && ( {exp.data.technologies && exp.data.technologies.length > 0 && (
<div class="flex flex-wrap gap-1.5"> <div class="flex flex-wrap gap-1.5">
{exp.data.technologies.map((tech: string) => ( {exp.data.technologies.map((tech: string) => (
<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/[0.06] text-white/50 border border-white/[0.06]"> <span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
{tech} {tech}
</span> </span>
))} ))}
@ -92,3 +149,80 @@ function formatMonth(dateStr: string) {
</div> </div>
</section> </section>
</Layout> </Layout>
<style>
/* ── Light mode (default): white cards on purple gradient ── */
.exp-card {
background: white;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.exp-card--ongoing { box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.25), 0 1px 3px rgba(0, 0, 0, 0.08); }
.exp-card h3 { color: #111827 !important; }
.exp-card .exp-meta { color: #9ca3af; }
.exp-card .exp-company { color: #6b21a8; }
.exp-card .exp-company a { color: #6b21a8 !important; text-decoration: none !important; }
.exp-card .exp-company a:hover { color: #581c87 !important; }
.exp-card .exp-content,
.exp-card .exp-content p { color: #4b5563 !important; }
.exp-card .exp-content a { color: #6b21a8 !important; text-decoration-color: rgba(107, 33, 168, 0.3) !important; }
.exp-card .exp-content a:hover { color: #581c87 !important; }
.exp-card .exp-tech { background: #f3f4f6; color: #6b7280; border-color: #e5e7eb; }
.exp-logo { background: #f9fafb; border: 1px solid #e5e7eb; }
.exp-logo-fallback { background: #f3f4f6; border: 1px solid #e5e7eb; }
.exp-logo-fallback span { color: #9ca3af; }
/* Type badges light */
.type-badge-blue { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
.type-badge-amber { background: #fffbeb; color: #b45309; border-color: #fde68a; }
.type-badge-emerald { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
.type-badge-pink { background: #fdf2f8; color: #be185d; border-color: #fbcfe8; }
.type-badge-purple { background: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
/* Type borders light */
.type-border-blue { border-left-color: #3b82f6 !important; }
.type-border-amber { border-left-color: #f59e0b !important; }
.type-border-emerald { border-left-color: #10b981 !important; }
.type-border-pink { border-left-color: #ec4899 !important; }
.type-border-purple { border-left-color: #a855f7 !important; }
/* ── Dark mode: translucent cards ── */
:global(.dark) .exp-card {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
:global(.dark) .exp-card:hover { background: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-card--ongoing {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.12);
box-shadow: none;
}
:global(.dark) .exp-card h3 { color: white !important; }
:global(.dark) .exp-card .exp-meta { color: rgba(255, 255, 255, 0.3); }
:global(.dark) .exp-card .exp-company { color: #d4b5ff; }
:global(.dark) .exp-card .exp-company a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-company a:hover { color: white !important; }
:global(.dark) .exp-card .exp-content,
:global(.dark) .exp-card .exp-content p { color: rgba(255, 255, 255, 0.6) !important; }
:global(.dark) .exp-card .exp-content a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-content a:hover { color: white !important; }
:global(.dark) .exp-card .exp-tech { background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.45); border-color: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-logo { background: white; border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); }
:global(.dark) .exp-logo-fallback { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.1); }
:global(.dark) .exp-logo-fallback span { color: rgba(255, 255, 255, 0.4); }
/* Type badges dark */
:global(.dark) .type-badge-blue { background: rgba(96, 165, 250, 0.15); color: #93c5fd; border-color: rgba(96, 165, 250, 0.2); }
:global(.dark) .type-badge-amber { background: rgba(251, 191, 36, 0.15); color: #fcd34d; border-color: rgba(251, 191, 36, 0.2); }
:global(.dark) .type-badge-emerald { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.2); }
:global(.dark) .type-badge-pink { background: rgba(244, 114, 182, 0.15); color: #f9a8d4; border-color: rgba(244, 114, 182, 0.2); }
:global(.dark) .type-badge-purple { background: rgba(168, 85, 247, 0.15); color: #d4b5ff; border-color: rgba(168, 85, 247, 0.2); }
/* Type borders dark */
:global(.dark) .type-border-blue { border-left-color: rgba(96, 165, 250, 0.4) !important; }
:global(.dark) .type-border-amber { border-left-color: rgba(251, 191, 36, 0.4) !important; }
:global(.dark) .type-border-emerald { border-left-color: rgba(52, 211, 153, 0.4) !important; }
:global(.dark) .type-border-pink { border-left-color: rgba(244, 114, 182, 0.4) !important; }
:global(.dark) .type-border-purple { border-left-color: rgba(168, 85, 247, 0.4) !important; }
</style>