FieberPrints sells DIY punk shirts and linocut prints out of Berlin. The brief was to build something that felt as uncompromising as the product — dark backgrounds, zero border-radius, a magenta accent used as punctuation, not wallpaper. Full e-commerce: browsing, auth, cart, checkout, order history, admin dashboard. The visual identity and the engineering are the same project.
The hardest constraint wasn't technical. It was keeping every decision honest to the aesthetic.
FieberPrints has a specific customer: someone who buys handmade punk shirts, not someone browsing a polished lifestyle brand. Every UX decision was made with that person in mind. The dark background isn't atmosphere — it's the correct environment for photography of black and grey screen-printed garments. The zero border-radius isn't retro styling — it's a visual language that reads as hand-cut, immediate, no-nonsense.
The magenta (#fd00d4) appears on one thing: the primary call to action. Add to cart. Checkout. Confirm. It's used so sparingly that when it appears, it's impossible to miss. That's a deliberate information hierarchy decision — on a dark site with muted typography, the one thing that's always pink is the thing you're supposed to do next.
Order status badges follow the same logic: pending is orange-red (unresolved, needs attention), completed is green (done, calm), cancelled is desaturated red (final, distinct from pending). A customer reading their order history navigates by color before they read a word. That's the goal.
Product images had inconsistent backgrounds — real shirts shot against white, yellow, blue. Rather than hiding this, I locked every image to aspect-ratio: 3/4with object-fit: cover and added a 1px inset box-shadow against the dark site background. The inconsistency reads as intentional rawness rather than a production oversight.
Technical implementation follows ↓
Every layer of the stack was chosen to serve the product, not the résumé.
| Framework | Next.js 15 App Router for server components on gallery and product pages — only the cart, auth flows, and interactive overlays run client-side. Per-page CSS Modules with a shared globals layer keeps the bundle clean and the dark aesthetic enforced at the root. |
| Backend | Supabase Authentication (email/password + session persistence), artworks table (product data, image URLs, sizes, pricing), and orders table. Row-level security ensures users can only access their own order history — no custom middleware needed. |
| Design System | Modular CSS + CSS Variables Strict token architecture: --color-background, --color-surface, --color-surface-alt for three depth levels; --color-secondary reserved for primary CTAs only; --font-display (Bebas Neue) and --font-body (Space Grotesk) enforced globally. Every component imports its own .module.css. No Tailwind, no CSS-in-JS — the constraint was the point. |
| State | React Context + useState Cart state lives in a context provider at layout level — nav badge, cart drawer, and checkout page share one source of truth. Auth state derives from Supabase's onAuthStateChange listener. No external store needed at this scope. |
| Payments | Stripe Stripe Checkout for payment flow. Server-side session creation keeps secret keys off the client. Order confirmation writes back to Supabase via a webhook, so order history is always consistent with payment state. |
CSS defaults fight dark, zero-radius design. Every component needed explicit intent.
Browser defaults constantly push back against a dark, borderless aesthetic — button styles, input focus rings, link colors, background fills. The token system was the answer: surface colors and text colors are explicitly separated so no component can accidentally use a text token as a background. If something renders wrong, the token name tells you exactly why.
The ticker animation required a specific architectural fix. A single strip withtranslateX(-50%) produces a gap mid-loop whenever content width doesn't divide evenly into the viewport. Two identical strips side-by-side, each animating at translateX(-100%), solves it completely — the second strip is always immediately adjacent to the first, so no gap is geometrically possible regardless of content length.
/* Two strips. Each moves its own full width left. No gap. */
.ticker {
display: flex; /* strips sit side by side */
overflow: hidden;
width: 100%;
}
.tickerInner {
min-width: 100%; /* always fills viewport */
animation: tickerScroll 22s linear infinite;
}
@keyframes tickerScroll {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}