Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/systems-witch/llms.txt

Use this file to discover all available pages before exploring further.

The Skills page translates the traditional skill-percentage chart into a set of tarot cards. Each card holds a category label, a dashed-square icon that spins on hover, the skill name in Cinzel serif italic, and a horizontal progress bar whose fill animates from zero when the card scrolls into the viewport. The aspect-[2/3] ratio keeps all cards uniformly tall-and-narrow regardless of their content, enforcing the tarot deck metaphor.

Data structure

The six default skills are defined in a const skills array above the component:
const skills = [
  { name: "React / Next.js", level: 90, category: "Frontend" },
  { name: "TypeScript",      level: 85, category: "Core" },
  { name: "Node.js",         level: 75, category: "Backend" },
  { name: "WebGL / Three.js",level: 60, category: "Creative" },
  { name: "Tailwind CSS",    level: 95, category: "Design" },
  { name: "System Architecture", level: 80, category: "Core" }
];
SkillLevelCategory
React / Next.js90%Frontend
TypeScript85%Core
Node.js75%Backend
WebGL / Three.js60%Creative
Tailwind CSS95%Design
System Architecture80%Core

Grid layout

Cards are arranged in a grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 — one column on mobile, two from 640px, three from 1024px. Each card uses aspect-[2/3] to maintain its portrait ratio across all breakpoints.

Card anatomy

Each card is a motion.div with a spring entry animation and a full hover effect:
<motion.div
  initial={{ opacity: 0, y: 50 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ delay: index * 0.1, type: "spring", stiffness: 100 }}
  className="relative aspect-[2/3] border border-graphite bg-black p-6
             flex flex-col items-center justify-between
             group hover:border-acid transition-colors hover:z-10
             hover:-translate-y-4 duration-300"
>
The card uses flex flex-col items-center justify-between to anchor three content zones — top, middle, and bottom — to the card extremities.

Top row: index and category

<div className="w-full flex justify-between font-mono text-xs text-lilac">
  <span>{String(index + 1).padStart(2, "0")}</span>
  <span>{skill.category}</span>
</div>
The index is zero-padded to two characters (01, 02, …) with String.padStart. The category string is right-aligned.

Center icon: circle and dashed square

<div className="w-24 h-24 rounded-full border border-graphite flex items-center justify-center
                group-hover:border-acid group-hover:shadow-glow transition-all duration-500">
  <div className="w-12 h-12 border border-dashed border-lilac
                  group-hover:border-acid group-hover:rotate-180 transition-all duration-1000" />
</div>
On hover:
  • The outer circle border transitions from graphite to acid and gains shadow-glow (box-shadow: 0 0 10px rgba(164,255,61,.5))
  • The inner dashed square border transitions from lilac to acid and rotates 180° over 1000ms
The combined effect reads as the icon “activating” — the geometric sigil awakening.

Bottom zone: name and progress bar

The skill name uses font-serif italic text-xl text-bone, rendering in Cinzel italic — the same typeface used for the Witch part of the home page wordmark. On hover it transitions to text-acid. The progress bar is a two-layer construction:
<div className="w-full h-1 bg-graphite relative overflow-hidden">
  <motion.div
    className="absolute top-0 left-0 h-full bg-acid"
    initial={{ width: 0 }}
    whileInView={{ width: `${skill.level}%` }}
    viewport={{ once: true }}
    transition={{ duration: 1, delay: 0.5 }}
  />
</div>
The track is a h-1 bg-graphite bar. The fill is a motion.div with whileInView — it starts at width: 0 and animates to ${skill.level}% when the card enters the viewport. viewport: { once: true } means the animation fires only on first scroll-into-view, not on every re-entry. The 0.5-second delay lets the card’s own entry animation finish before the bar starts filling. A font-mono text-xs text-right text-acid readout below the bar shows the numeric percentage.

Hover effects summary

ElementDefaultOn group-hover
Card borderborder-graphiteborder-acid
Card positionin-flowlifted hover:-translate-y-4
Outer ring borderborder-graphiteborder-acid + shadow-glow
Inner dashed squareborder-lilacborder-acid + rotate-180
Skill nametext-bonetext-acid
The hover:-translate-y-4 lift and hover:z-10 elevation are both applied directly via Tailwind utility classes, not Framer Motion — this is intentional CSS hover, not an animation. The Framer Motion on the card handles only the scroll-entry animation.

Customization

Add or modify a skill: Edit the skills array. Each entry needs three fields:
{ name: "Your Skill Name", level: 70, category: "YourCategory" }
  • name — displayed in Cinzel italic; keep it short enough to fit on one line
  • level — integer 0–100; drives both the progress bar width and the percentage readout
  • category — displayed top-right in the card header in text-lilac; no validation, any string works
Reorder cards: The cards render in array order. Re-arrange the objects in the skills array to change the visual sequence. The stagger delay is index-based, so the new first card will always animate in first. Change animation timing: The progress bar duration: 1 and delay: 0.5 can be adjusted per card by moving the animation config inside the .map() callback. To trigger the bar on mount instead of scroll, replace whileInView with animate.
If you want all six bars to fill simultaneously when any one of them enters the viewport (rather than each triggering independently), wrap the entire grid in a single motion.div with whileInView and pass a Framer Motion variants object down to each bar via animate instead of whileInView.

Build docs developers (and LLMs) love