What does drawn-notebook-ui do?
name: drawn-notebook-ui description: > A complete design system and implementation guide for reproducing the "Drawn Notebook" aesthetic — a quiet, editorial, hand-crafted UI language originally authored for Askar's personal portfolio. Covers every token, component, animation, and interaction pattern in exhaustive detail for both Flutter (Dart) and vanilla web (HTML/CSS/JS), in both light and dark modes. Use this skill whenever the user wants to build, adapt, or extend this specific visual language in any context.
Overview
The Drawn Notebook is not a component library. It is a design language — a coherent philosophy that every pixel, every line weight, every spacing rhythm, and every animation timing curve serves. Its governing principle is that the interface should feel like a living notebook: ink drawn on cream paper, passages revealed slowly as the reader scrolls down, navigational threads that traverse the page like a physical journey. Nothing is loud. Nothing is decorative for its own sake. The UI earns every visual element by anchoring it to meaning.
This skill is a precise, comprehensive reference. When implementing this UI, follow every section in order. Do not skip the token definitions — every spacing, color, and motion value is load-bearing. Do not skip the dark mode section — both themes are equal first-class citizens and must be simultaneously correct at all times.
Section 1 — Design Philosophy
1.1 Core Principles
Quiet confidence, not minimalism for its own sake. The design is spare, but every element that remains is purposeful. Blank space is not laziness — it is the page breathing. The user's eye is guided by composition, weight, and motion, not by color saturation or decorative flourishes.
Hand-drawn aesthetic without literal skeuomorphism. The UI references the craft of drawing — thin stroke weights, serif editorial type, paper-grain textures, SVG illustrations rendered as line drawings — but it does not pretend to be paper. It is a digital surface that respects the tactility of analog craft without mimicking it clumsily.
Every section is a chapter. The page is a continuous scroll, and each section feels like turning to a new page in a notebook. Section backgrounds shift subtly between warm neutrals. A continuous golden thread (the journey line SVG) stitches them together visually.
Motion is narration, not entertainment. Animations reveal content the way a hand writes words — appearing from bottom to top, fading in softly, tracing SVG paths. Durations are slow by modern standards (1.2s–2s for reveals). This is intentional. Speed implies noise. Slowness implies craft.
Avoid AI slop patterns. The following patterns are explicitly forbidden: gradient hero backgrounds with purple-to-pink transitions, card grids with identical height and identical corner radii, hover states that glow neon, oversized rounded corners (> 14px on most elements), color-coded skill bars with percentage numbers, timeline with bullet points and bold dates, and any use of emoji as decoration.
Section 2 — Design Tokens
2.1 Color Palette — Light Mode (Daylight Notebook)
Every color in light mode is a warm neutral derived from paper, cream, and natural ink. There is a single accent family: gold/terracotta, ranging from a muted amber to a warm brick.
/* Light Mode (applied to :root) */
--cream: #F7F5F0; /* Page background — primary surface */
--sage: #EEF3ED; /* About section background — the lightest green-grey */
--parchment: #F1ECE3; /* Experience + Resume section background */
--warm-white: #FAF8F5; /* Skills section background — the lightest surface */
--linen: #F3EFE8; /* Projects section background */
--sage-tint: #EFF3EE; /* Learning section background */
--charcoal: #1A1A1A; /* Primary text, SVG strokes, borders */
--soft-black: #2C2C2C; /* Secondary text, strong labels */
--warm-grey: #8A8580; /* Muted labels, timestamps, captions */
--light-grey: #C5BFB8; /* Scroll prompt, decorative separators */
--terracotta: #B87A5A; /* Hover states, bullets, active waypoints */
--indigo: #6B6EA0; /* Reserved — not used in primary patterns */
--gold: #C4956A; /* Primary accent — journey line, skill icons, chips */
--gold-dim: rgba(196, 149, 106, 0.3); /* Soft gold for shadows, borders */
--blush: #F0EBE5; /* Chip/badge backgrounds in hover */
--sage-dark: #8B9E8B; /* Section border separator tint */
2.2 Color Palette — Dark Mode (Night Notebook)
Dark mode is not a simple inversion. The backgrounds shift from warm creams to deep warm-blacks — an almost ink-stained quality. The accent colors brighten slightly to maintain legibility against dark surfaces.
/* Dark Mode (applied to :root[data-theme="dark"]) */
--cream: #141210; /* Page background — deep warm black */
--sage: #171816; /* About section background */
--parchment: #1A1815; /* Experience + Resume background */
--warm-white: #1E1B18; /* Skills section background */
--linen: #181614; /* Projects section background */
--sage-tint: #161815; /* Learning section background */
--charcoal: #EDE8E1; /* Primary text — warm off-white */
--soft-black: #D8D2C8; /* Secondary text */
--warm-grey: #9E978F; /* Muted labels */
--light-grey: #38342E; /* Decorative separators */
--terracotta: #D98A62; /* Hover accent — slightly brighter than light mode */
--indigo: #8C8FB8;
--gold: #D4A373; /* Primary accent — slightly warmer than light mode */
--gold-dim: rgba(212, 163, 115, 0.3);
--blush: #241E1A;
--sage-dark: #9EAF9E;
/* Surface tokens */
--card-glass: rgba(30, 27, 24, 0.72);
--card-glass-hover: rgba(42, 38, 34, 0.95);
--card-item-bg: rgba(24, 21, 19, 0.78);
--card-item-hover: rgba(38, 34, 30, 0.96);
--card-border: rgba(212, 163, 115, 0.22);
--card-border-hover: rgba(212, 163, 115, 0.48);
--card-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
--card-shadow-hover: 0 6px 20px rgba(0, 0, 0, 0.45);
--card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.05);
--card-inset-hover: inset 0 1px 0 rgba(255, 255, 255, 0.08);
--badge-bg: rgba(24, 21, 19, 0.88);
--grain-opacity: 0.022; /* Lower in dark to avoid muddiness */
2.3 Color Palette — Light Mode Surface Tokens
/* Light Mode surface tokens */
--card-glass: rgba(255, 255, 255, 0.65);
--card-glass-hover: #ffffff;
--card-item-bg: rgba(247, 245, 240, 0.8);
--card-item-hover: rgba(255, 255, 255, 0.98);
--card-border: rgba(196, 149, 106, 0.22);
--card-border-hover: rgba(196, 149, 106, 0.45);
--card-shadow: 0 4px 16px rgba(26, 26, 26, 0.04);
--card-shadow-hover: 0 6px 20px rgba(196, 149, 106, 0.12);
--card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9);
--card-inset-hover: inset 0 1px 0 rgba(255, 255, 255, 0.95);
--badge-bg: rgba(247, 245, 240, 0.82);
--grain-opacity: 0.035;
2.4 Theme Switching — Flash-Free Implementation
Theme detection must run synchronously in the <head> before the page renders, to prevent a flash of wrong theme (FOUT). This is a critical implementation requirement:
<head>
<!-- MUST be the first script in <head>, before any stylesheet links -->
<script>
(function () {
try {
var stored = localStorage.getItem('color-scheme');
var prefersDark = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = (stored === 'dark' || (!stored && prefersDark)) ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
var meta = document.querySelector('meta[name="color-scheme"]');
if (meta && (stored === 'dark' || stored === 'light')) meta.content = stored;
var themeColor = document.querySelector('meta[name="theme-color"]');
if (themeColor) themeColor.content = theme === 'dark' ? '#141210' : '#F8F4F0';
} catch (e) {}
})();
</script>
</head>
In Flutter/Dart, use ThemeMode.system as the default, with a persisted override via SharedPreferences. The key is color-scheme, the value is 'dark' or 'light'.
2.5 Typography Tokens
--font-serif: 'Cormorant Garamond', 'Georgia', serif;
--font-sans: 'Inter', 'Helvetica Neue', sans-serif;
Cormorant Garamond is non-negotiable for the editorial voice of this design. It is loaded at weights 300, 400, 500, 600, with italic variants for 300 and 400. It is used for all headings, section titles, the hero name, chips, rail labels, waypoint messages, and anywhere a "written" quality is needed. It must never be used at weights above 600 — this is not a bold typeface.
Inter is the functional companion. It is used for captions, skill tags, timestamps, uppercase kickers, and body text in dense information contexts. Load weights 300, 400, 500 only.
/* Google Fonts import */
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=Inter:wght@300;400;500&display=swap');
2.6 Spacing Tokens
All spacing is fluid using clamp(). Do not use hard pixel values for section padding or content width constraints.
--section-pad: clamp(80px, 12vh, 160px); /* Vertical padding for major sections */
--content-max: 800px; /* Narrow prose column */
--content-wide: 1100px; /* Wide layout for grids */
--side-pad: clamp(24px, 5vw, 80px); /* Horizontal page margins */
2.7 Animation Tokens
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); /* General ease-out */
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); /* Material-like smooth */
--duration-slow: 1.2s; /* Reveal animations */
--duration-med: 0.8s; /* Medium transitions */
--duration-fast: 0.4s; /* Quick micro-interactions */
Section 3 — Global Foundations
3.1 CSS Reset and Base
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: var(--cream);
color: var(--charcoal);
font-family: var(--font-sans);
font-weight: 300;
line-height: 1.7;
overflow-x: hidden;
position: relative;
}
3.2 Text Selection
The selection highlight is a soft gold wash — it reflects the accent palette without being aggressive:
::selection {
background: rgba(196, 149, 106, 0.2);
color: var(--charcoal);
}
::-moz-selection {
background: rgba(196, 149, 106, 0.2);
color: var(--charcoal);
}
3.3 SVG Stroke Adaptation
All inline SVGs use hardcoded hex strokes (#1A1A1A, #2C2C2C) from the light mode palette. To adapt them to dark mode without touching the SVG markup, use a CSS attribute selector override:
svg [stroke="#1A1A1A"], svg [stroke="#1a1a1a"] { stroke: var(--charcoal); }
svg [fill="#1A1A1A"], svg [fill="#1a1a1a"] { fill: var(--charcoal); }
svg [stroke="#2C2C2C"], svg [stroke="#2c2c2c"] { stroke: var(--soft-black); }
svg [fill="#2C2C2C"], svg [fill="#2c2c2c"] { fill: var(--soft-black); }
svg text[fill="#1A1A1A"] { fill: var(--charcoal); }
svg text[fill="#8A8580"] { fill: var(--warm-grey); }
Gold accents (#C4956A) in SVGs are intentionally NOT overridden. The gold accent is a fixed reference color in both themes; in dark mode the token --gold brightens to #D4A373, but SVG inline gold does not need remapping because the contrast against dark backgrounds is still acceptable.
3.4 Smooth Theme Transition
Apply a coordinated transition to every surface that changes between themes. This prevents jarring color jumps:
body, section,
.about, .experience, .resume-section, .skills, .projects, .learning, .closing,
.exp-card, .exp-item, .skill-cell, .compact-card, .compact-card__icon,
.about__point, .about__visual-card, .about__chip, .closing__link,
.journey-waypoint__message, .rail-node__label, .resume-modal__panel,
.resume-modal__header {
transition:
background-color 0.35s var(--ease-out),
color 0.35s var(--ease-out),
border-color 0.35s var(--ease-out);
}
/* Always respect user motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; }
}
Section 4 — Paper Grain Overlay
A subtle paper-grain texture is applied as a full-viewport fixed overlay. It sits at z-index: 9999 — above everything — but pointer-events: none ensures it never intercepts interactions.
.grain-overlay {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
opacity: var(--grain-opacity, 0.035);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 200px 200px;
transform: translateZ(0);
will-change: transform;
transition: opacity 0.35s var(--ease-out);
}
/* In dark mode, screen blend reveals the grain more naturally */
:root[data-theme="dark"] .grain-overlay {
mix-blend-mode: screen;
}
In Flutter: Reproduce this using a CustomPaint widget at the top of the widget tree with a shader-based noise pattern, or bake the grain as a PNG asset with ~3% opacity and use BlendMode.screen in dark mode.
Section 5 — Theme Toggle (Celestial Switcher)
The theme toggle is a small pill button positioned fixed in the top-right corner. It displays a sun icon in light mode and a moon icon in dark mode. Both icons coexist in the DOM and are shown/hidden via CSS opacity — no JavaScript rendering is needed for the initial icon state.
5.1 Layout and Positioning
.theme-switch {
position: fixed;
top: clamp(16px, 2.5vw, 24px);
right: clamp(16px, 3vw, 28px);
z-index: 1001;
}
5.2 Button Styling
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.85rem 0.35rem 0.45rem;
background: var(--badge-bg);
border: 1px solid var(--card-border);
border-radius: 999px;
cursor: pointer;
color: var(--charcoal);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
transition:
transform 0.25s var(--ease-out),
background 0.3s var(--ease-out),
border-color 0.3s var(--ease-out),
box-shadow 0.25s var(--ease-out);
user-select: none;
}
.theme-toggle:hover {
transform: translateY(-1px);
border-color: var(--gold);
box-shadow: 0 4px 14px var(--gold-dim);
}
.theme-toggle:active {
transform: translateY(0) scale(0.97);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 3px;
}
5.3 Icon Switching (CSS-only, no JS flicker)
The sun and moon icons both live in the DOM. CSS handles visibility based on the data-theme attribute:
/* LIGHT MODE: show sun, hide moon */
:root:not([data-theme="dark"]) .theme-toggle__icon--light {
opacity: 1;
transform: rotate(0deg) scale(1);
}
:root:not([data-theme="dark"]) .theme-toggle__icon--dark {
opacity: 0;
transform: rotate(-90deg) scale(0.5);
pointer-events: none;
}
/* DARK MODE: show moon, hide sun */
:root[data-theme="dark"] .theme-toggle__icon--light {
opacity: 0;
transform: rotate(90deg) scale(0.5);
pointer-events: none;
}
:root[data-theme="dark"] .theme-toggle__icon--dark {
opacity: 1;
transform: rotate(0deg) scale(1);
}
.theme-toggle__icon {
position: absolute;
inset: 0;
margin: auto;
color: var(--gold);
transition:
opacity 0.35s var(--ease-out),
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); /* Spring bounce on appear */
}
5.4 Label Text
The label reads "night mode" in light theme and "day mode" in dark theme. It is styled in Cormorant Garamond italic:
.theme-toggle__text {
font-family: var(--font-serif);
font-style: italic;
font-size: 0.84rem;
letter-spacing: 0.03em;
color: var(--charcoal);
line-height: 1;
}
On mobile (< 600px), hide the text and show only the icon disc:
@media (max-width: 600px) {
.theme-toggle {
padding: 0;
width: 36px;
height: 36px;
justify-content: center;
}
.theme-toggle__text { display: none; }
}
5.5 Flutter Implementation
In Flutter, use a GestureDetector or IconButton wrapping a custom AnimatedSwitcher that transitions between a Icon(Icons.wb_sunny_outlined) and a moon SVG. Store the theme in ThemeMode via a ChangeNotifier provider. Persist to SharedPreferences with key color-scheme.
Section 6 — Section Title (Shared Typography)
Every section uses the same title treatment: Cormorant Garamond, weight 300, with a short 40px golden underline rule.
.section-title {
font-family: var(--font-serif);
font-weight: 300;
font-size: clamp(1.8rem, 4vw, 3rem);
letter-spacing: 0.06em;
color: var(--charcoal);
margin-bottom: 3rem;
position: relative;
}
.section-title::after {
content: '';
display: block;
width: 40px;
height: 1px;
background: var(--gold);
margin-top: 1rem;
opacity: 0.5;
}
This underline is a deliberate constraint: 40px, 1px tall, 50% opacity gold. Never make it wider, thicker, or more opaque. It is a punctuation mark, not a decoration.
Section 7 — Reveal Animations (Scroll-Triggered)
Content is hidden by default and revealed as it scrolls into view. This system uses IntersectionObserver and CSS transitions, not JS-driven style mutations.
7.1 Initial Hidden State
.reveal-item {
opacity: 0;
transform: translateY(24px);
transition:
opacity var(--duration-slow) var(--ease-out),
transform var(--duration-slow) var(--ease-out);
}
.reveal-item.is-visible {
opacity: 1;
transform: translateY(0);
}
7.2 Stagger Children
Group elements with .reveal-group and stagger each .reveal-item child:
.reveal-group .reveal-item:nth-child(1) { transition-delay: 0s; }
.reveal-group .reveal-item:nth-child(2) { transition-delay: 0.1s; }
.reveal-group .reveal-item:nth-child(3) { transition-delay: 0.2s; }
.reveal-group .reveal-item:nth-child(4) { transition-delay: 0.3s; }
/* ... up to nth-child(10) at 0.9s delay */
7.3 SVG Draw-Line Animation
For SVG paths that should "draw themselves" into view:
.draw-line {
stroke-dasharray: 500;
stroke-dashoffset: 500;
transition: stroke-dashoffset 2s var(--ease-smooth);
}
.is-visible .draw-line,
.draw-line.is-drawn {
stroke-dashoffset: 0;
}
7.4 Intersection Observer (JavaScript)
function initRevealObserver() {
const revealItems = document.querySelectorAll('.reveal-item');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
}
});
}, {
root: null,
rootMargin: '0px 0px -60px 0px',
threshold: 0.1
});
revealItems.forEach(item => observer.observe(item));
}
7.5 Flutter Implementation
In Flutter, use AnimationController with CurvedAnimation(curve: Curves.easeOut) and a SlideTransition from Offset(0, 0.3) to Offset.zero, combined with a FadeTransition. Trigger via a VisibilityDetector widget (package: visibility_detector) or scroll controller listeners. Stagger delays using Future.delayed per item index multiplied by 100ms.
Section 8 — Journey Line (Continuous Golden Thread)
A thin golden SVG path runs continuously from the top of the page to the bottom, stitching all sections together like a physical thread through the narrative. It is positioned absolutely, z-index: 6, and traces a meandering organic path calculated by JavaScript from the positions of journey waypoints.
8.1 SVG Container
.journey-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 6;
transform: translateZ(0);
}
.thread-path {
stroke: var(--gold);
stroke-width: 1.4px;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.18; /* Subtle — present, not distracting */
}
The path d attribute is generated dynamically in JS based on the positions of .journey-waypoint dots at layout time and on resize.
Section 9 — Journey Waypoints
Small interactive dots that anchor the journey line at specific narrative points across the page. Each waypoint has a floating tooltip that reveals on hover via CSS.
9.1 Dot Styling
.journey-waypoint {
position: relative;
display: flex;
width: fit-content;
align-items: center;
justify-content: center;
cursor: pointer;
pointer-events: auto;
user-select: none;
padding: 6px; /* Generous hit target around 8px dot */
margin-bottom: calc(0.75rem - 6px);
line-height: 1;
}
.journey-waypoint__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--gold);
opacity: 0.45;
transition: transform 0.3s var(--ease-out), opacity 0.3s var(--ease-out), background-color 0.3s var(--ease-out);
}
.journey-waypoint:hover .journey-waypoint__dot,
.journey-waypoint.is-active .journey-waypoint__dot {
transform: scale(1.5);
opacity: 1;
background: var(--terracotta);
}
9.2 Positional Variants
Waypoints are placed at different horizontal positions across sections to create an organic traversal pattern. Three variants:
.journey-waypoint--left { margin-left: 0; margin-right: auto; }
.journey-waypoint--center { margin-left: clamp(80px, 25vw, 380px); margin-right: auto; }
.journey-waypoint--right { margin-left: clamp(160px, 48vw, 680px); margin-right: auto; }
9.3 Floating Tooltip
.journey-waypoint__message {
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%) translateX(-4px);
white-space: nowrap;
font-family: var(--font-serif);
font-style: italic;
font-size: 0.84rem;
letter-spacing: 0.02em;
color: var(--charcoal);
background: var(--badge-bg);
backdrop-filter: blur(8px);
border: 1px solid var(--card-border);
border-radius: 999px;
padding: 0.32rem 0.85rem;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.25s var(--ease-out), transform 0.25s var(--ease-out), visibility 0.25s;
}
.journey-waypoint:hover .journey-waypoint__message,
.journey-waypoint:focus-visible .journey-waypoint__message {
opacity: 0.95;
visibility: visible;
transform: translateY(-50%) translateX(0);
}
/* Right-aligned waypoints open their tooltip inward to the left */
.journey-waypoint--right .journey-waypoint__message {
left: auto;
right: calc(100% + 12px);
transform: translateY(-50%) translateX(4px);
}
Section 10 — Floating Side Rail Navigation
A fixed vertical rail on the right side of the viewport provides non-intrusive section navigation. Only visible at > 960px wide. Contains small dots for each section with hover labels.
10.1 Rail Positioning
.journey-rail {
position: fixed;
right: 24px;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
display: flex;
align-items: center;
pointer-events: auto;
}
@media (max-width: 960px) {
.journey-rail { display: none; }
}
10.2 Track and Progress
A thin vertical line serves as the track. A --progress div grows from top based on scroll position:
.journey-rail__track {
position: absolute;
left: 50%;
top: 14px; bottom: 14px;
width: 1.5px;
background: rgba(196, 149, 106, 0.2);
transform: translateX(-50%);
border-radius: 999px;
}
.journey-rail__progress {
width: 100%;
height: 0%;
background: var(--terracotta);
border-radius: 999px;
transition: height 0.15s ease-out;
}
10.3 Rail Node Dots
.rail-node {
position: relative;
width: 28px; height: 28px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
outline: none;
}
.rail-node__dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--cream);
border: 1.5px solid rgba(196, 149, 106, 0.45);
transition: transform 0.35s var(--ease-out), background-color 0.35s var(--ease-out), border-color 0.35s var(--ease-out), box-shadow 0.35s var(--ease-out);
}
.rail-node:hover .rail-node__dot,
.rail-node.is-active .rail-node__dot {
transform: scale(1.4);
background: var(--terracotta);
border-color: var(--terracotta);
box-shadow: 0 0 12px rgba(196, 149, 106, 0.5);
}
10.4 Rail Node Labels
Each rail node has a floating label that appears to its left. The label uses a two-part structure: a colored section tag and a plain section name.
.rail-node__label {
position: absolute;
right: calc(100% + 14px);
top: 50%;
transform: translateY(-50%) translateX(6px);
padding: 0.35rem 0.85rem;
background: var(--badge-bg);
backdrop-filter: blur(8px);
border: 1px solid var(--card-border);
border-radius: 999px;
white-space: nowrap;
display: inline-flex; align-items: center; gap: 0.45rem;
opacity: 0; visibility: hidden; pointer-events: none;
transition: opacity 0.25s var(--ease-out), transform 0.25s var(--ease-out);
}
.rail-node__tag {
font-family: var(--font-serif);
font-style: italic;
font-size: 0.82rem;
color: var(--terracotta);
font-weight: 500;
letter-spacing: 0.02em;
}
.rail-node__text {
font-family: var(--font-serif);
font-style: italic;
font-size: 0.82rem;
color: var(--charcoal);
opacity: 0.88;
}
.rail-node:hover .rail-node__label,
.rail-node:focus-visible .rail-node__label {
opacity: 0.95; visibility: visible;
transform: translateY(-50%) translateX(0);
}
Section 11 — Hero Section
The hero is a full-viewport-height centred section with multiple layers of ambient visuals.
11.1 Hero Container
.hero {
position: relative;
min-height: clamp(520px, 82vh, 740px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: clamp(1.5rem, 3vh, 2.5rem) var(--side-pad);
z-index: 5;
overflow: hidden;
}
11.2 Animated Entrance Circle
A 105×105px SVG ring draws itself around a central 8-pointed star. The ring is drawn using stroke-dasharray animation:
@keyframes drawCircle {
to { stroke-dashoffset: 60; } /* Intentionally incomplete — leaves a gap */
}
@keyframes starAppear {
to { opacity: 0.7; }
}
@keyframes lineDescend {
to { height: 44px; }
}
The descending line below the circle is a 1px wide element growing from 0 to 44px with a gold-to-transparent gradient — it visually connects the circle to the text below.
11.3 Hero Name and Tagline
.hero__name {
font-family: var(--font-serif);
font-weight: 300;
font-size: clamp(2.35rem, 5.8vw, 4.4rem);
letter-spacing: 0.15em; /* Wide tracking — editorial, not tight */
color: var(--charcoal);
line-height: 1.1;
animation: textFadeUp 1.2s var(--ease-out) 1s forwards;
opacity: 0;
}
.hero__tagline {
font-family: var(--font-sans);
font-weight: 300;
font-size: clamp(0.78rem, 1.3vw, 0.88rem);
letter-spacing: 0.08em;
color: var(--warm-grey);
margin-top: 0.75rem;
animation: textFadeUp 1.2s var(--ease-out) 1.8s forwards;
opacity: 0;
}
.hero__reflection {
font-family: var(--font-serif);
font-weight: 300;
font-style: italic;
font-size: clamp(0.85rem, 1.3vw, 0.98rem);
line-height: 1.6;
max-width: 480px;
margin: 1.15rem auto 0;
color: var(--soft-black);
animation: textFadeUp 1.2s var(--ease-out) 2.2s forwards;
opacity: 0;
}
@keyframes textFadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
11.4 Floating Skill Icons
20 small SVG icon elements orbit the perimeter of the hero section, two columns left, two columns right. They float with continuous gentle motion using three alternating CSS keyframe sequences (A, B, C) at varying durations between 8.5s and 12.5s. Each item fades in with a staggered delay from 1.5s to 3.4s.
.hero__float-item {
position: absolute;
pointer-events: auto;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: var(--charcoal);
opacity: 0;
will-change: transform;
}
.hero__float-item svg {
opacity: 0.36; /* Muted at rest */
transition: opacity 0.3s var(--ease-out), transform 0.4s var(--ease-out);
}
.hero__float-item:hover svg {
opacity: 0.95;
transform: scale(1.2);
}
.hero__float-label {
font-family: var(--font-serif);
font-size: 0.68rem;
font-style: italic;
letter-spacing: 0.06em;
color: var(--terracotta);
opacity: 0; /* Hidden until hover */
transform: translateY(4px);
transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out);
white-space: nowrap;
}
.hero__float-item:hover .hero__float-label {
opacity: 0.95;
transform: translateY(0);
}
Float motion keyframes — three variants ensuring no two adjacent items move identically:
@keyframes floatMotionA {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
33% { transform: translate(5px, -8px) rotate(2deg); }
66% { transform: translate(-4px, 6px) rotate(-1.5deg); }
}
@keyframes floatMotionB {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
33% { transform: translate(-6px, 8px) rotate(-2.5deg); }
66% { transform: translate(6px, -5px) rotate(2deg); }
}
@keyframes floatMotionC {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
33% { transform: translate(6px, 6px) rotate(2deg); }
66% { transform: translate(-5px, -7px) rotate(-2deg); }
}
11.5 Scroll Prompt
Positioned absolutely at the bottom-center of the hero. The arrow bounces gently:
.hero__scroll-prompt {
position: absolute;
bottom: clamp(14px, 2.5vh, 24px);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.hero__scroll-prompt span {
font-family: var(--font-serif);
font-size: 0.75rem;
letter-spacing: 0.12em;
color: var(--light-grey);
font-style: italic;
}
@keyframes scrollBounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(6px); }
}
Section 12 — About Section
The About section uses a two-column layout: narrative text with bullet points on the left; architectural SVG illustrations and chips on the right.
12.1 Section Background
.about {
position: relative;
padding: var(--section-pad) var(--side-pad);
z-index: 5;
background: var(--sage);
border-top: 1px solid rgba(139, 158, 139, 0.22);
}
12.2 Two-Column Grid
.about__body {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
max-width: var(--content-wide);
margin: 0 auto;
align-items: start;
}
@media (min-width: 860px) {
.about__body {
grid-template-columns: 1.18fr 0.82fr;
gap: clamp(2.5rem, 5vw, 5rem);
}
}
12.3 Lead Paragraph
.about__lead {
font-family: var(--font-serif);
font-weight: 300;
font-size: clamp(1.05rem, 1.6vw, 1.22rem);
line-height: 1.85;
color: var(--charcoal);
margin-bottom: 1.6rem;
}
.about__lead strong {
font-weight: 500;
color: var(--soft-black);
}
12.4 Bullet Points (Glassmorphic Cards)
Each bullet is a translucent card, not a plain list item. The ✦ bullet is a terracotta Unicode character that rotates 45° and scales on hover:
.about__point {
display: flex;
align-items: flex-start;
gap: 0.85rem;
padding: 0.85rem 1.1rem;
background: var(--card-glass);
border: 1px solid var(--card-border);
border-radius: 8px;
transition:
background 0.3s var(--ease-out),
border-color 0.3s var(--ease-out),
transform 0.3s var(--ease-out),
box-shadow 0.3s var(--ease-out);
backdrop-filter: blur(4px);
}
.about__point:hover {
background: var(--card-glass-hover);
border-color: var(--card-border-hover);
transform: translateX(4px); /* Slide right on hover */
box-shadow: 0 4px 14px rgba(139, 158, 139, 0.15);
}
.about__point-bullet {
font-size: 0.75rem;
color: var(--terracotta);
line-height: 1.7;
flex-shrink: 0;
transition: transform 0.3s var(--ease-out);
}
.about__point:hover .about__point-bullet {
transform: scale(1.3) rotate(45deg); /* The ✦ star rotates on hover */
}
.about__point-text {
font-family: var(--font-serif);
font-size: clamp(0.95rem, 1.35vw, 1.05rem);
line-height: 1.65;
color: var(--charcoal);
}
.about__point-text strong {
font-weight: 500;
color: var(--soft-black);
font-family: var(--font-sans);
font-size: 0.85rem;
letter-spacing: 0.01em;
}
.about__point-text em {
font-style: italic;
color: var(--terracotta);
}
12.5 Visual Cards (Right Column)
Small cards displaying SVG architectural diagrams. Two in a 2-column grid, one full-width below:
.about__visual-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 1.5rem 1rem;
background: var(--card-glass);
border: 1px solid var(--card-border);
border-radius: 12px;
gap: 0.75rem;
backdrop-filter: blur(4px);
transition:
background 0.35s var(--ease-out),
border-color 0.35s var(--ease-out),
transform 0.35s var(--ease-out),
box-shadow 0.35s var(--ease-out);
}
.about__visual-card:hover {
background: var(--card-glass-hover);
border-color: var(--card-border-hover);
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(196, 149, 106, 0.12);
}
.about__visual-caption {
font-family: var(--font-serif);
font-size: 0.76rem;
font-style: italic;
color: var(--warm-grey);
letter-spacing: 0.04em;
}
12.6 Chips
Pill-shaped badges below the visual cards. Used to surface key facts quickly. Each chip has a small inline SVG icon:
.about__chip {
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.45rem 0.95rem;
background: var(--card-glass);
border: 1px solid var(--card-border);
border-radius: 24px;
font-family: var(--font-serif);
font-size: 0.88rem;
font-style: italic;
color: var(--soft-black);
letter-spacing: 0.02em;
backdrop-filter: blur(4px);
transition:
background 0.35s var(--ease-out),
border-color 0.35s var(--ease-out),
transform 0.35s var(--ease-out),
box-shadow 0.35s var(--ease-out);
}
.about__chip:hover {
background: var(--card-glass-hover);
border-color: var(--terracotta);
transform: translateY(-2px);
}
.about__chip-icon {
display: inline-flex;
align-items: center;
color: var(--terracotta);
flex-shrink: 0;
transition: transform 0.35s var(--ease-out);
}
.about__chip:hover .about__chip-icon {
transform: scale(1.15);
}
Section 13 — Experience Section (Timeline)
A vertical timeline with animated connecting thread. Each node has a circular SVG marker that draws itself when scrolled into view, plus a right-column illustrated card.
13.1 Section Background
.experience {
position: relative;
padding: clamp(2rem, 4vh, 3.5rem) var(--side-pad);
z-index: 5;
background: var(--parchment);
border-top: 1px solid rgba(196, 149, 106, 0.16);
}
13.2 Timeline Thread
A 1px vertical line with a gold gradient — fading in and out at the ends:
.timeline__thread {
position: absolute;
left: 17.5px;
top: 0; bottom: 0;
width: 1px;
background: linear-gradient(to bottom,
var(--gold-dim),
var(--gold) 20%,
var(--gold) 80%,
var(--gold-dim)
);
opacity: 0.4;
}
13.3 Node Marker (Animated Circle)
.timeline__marker .node-circle {
stroke-dasharray: 100;
stroke-dashoffset: 100;
transition: stroke-dashoffset 1.5s var(--ease-smooth);
}
.timeline__node.is-visible .node-circle {
stroke-dashoffset: 0; /* Circle draws itself when scrolled into view */
}
13.4 Timeline Content Layout
.timeline__content {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
}
@media (min-width: 820px) {
.timeline__content {
grid-template-columns: 1.15fr 0.95fr;
gap: clamp(1.25rem, 2.5vw, 2rem);
align-items: stretch;
}
}
13.5 Timeline Typography
.timeline__period {
font-family: var(--font-sans);
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--terracotta);
}
.timeline__company {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(1.35rem, 2.2vw, 1.65rem);
letter-spacing: 0.03em;
color: var(--charcoal);
}
.timeline__role {
font-family: var(--font-serif);
font-style: italic;
font-size: 0.85rem;
color: var(--warm-grey);
}
.timeline__details p {
font-size: 0.79rem;
line-height: 1.48;
color: var(--soft-black);
}
13.6 Award Pill
.exp-award-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.22rem 0.55rem;
background: rgba(196, 149, 106, 0.08);
border: 1px solid rgba(196, 149, 106, 0.28);
border-radius: 999px;
font-family: var(--font-sans);
font-size: 0.66rem;
color: var(--charcoal);
width: fit-content;
}
.exp-award-pill strong { color: var(--terracotta); font-weight: 600; }
.exp-award-star { color: var(--terracotta); font-size: 0.72rem; }
13.7 Experience Card (Right Column)
A compact glassmorphic card listing tools/technologies used in that role. Each entry (exp-item) is a small row with a 24px SVG icon and two lines of text:
.exp-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 0.35rem;
padding: 0.65rem 0.8rem;
background: var(--card-glass);
border: 1px solid var(--card-border);
border-radius: 10px;
backdrop-filter: blur(8px);
box-shadow: var(--card-shadow), var(--card-inset);
transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
}
.timeline__node:hover .exp-card {
border-color: var(--card-border-hover);
box-shadow: var(--card-shadow-hover), var(--card-inset-hover);
transform: translateY(-1px);
}
.exp-item {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.28rem 0.5rem;
background: var(--card-item-bg);
border: 1px solid var(--card-border);
border-radius: 7px;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.exp-item:hover {
background: var(--card-item-hover);
border-color: var(--card-border-hover);
transform: translateX(1px);
}
.exp-item__title {
font-family: var(--font-sans);
font-size: 0.69rem;
font-weight: 600;
color: var(--charcoal);
}
.exp-item__desc {
font-family: var(--font-sans);
font-size: 0.58rem;
color: var(--warm-grey);
}
.exp-card__footer {
display: flex;
align-items: center;
gap: 0.4rem;
padding-top: 0.28rem;
border-top: 1px solid rgba(196, 149, 106, 0.16);
font-family: var(--font-sans);
font-size: 0.59rem;
color: var(--warm-grey);
}
.exp-card__dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--gold);
}
Section 14 — Skills Section (Three-Pillar Staircase)
Three columns of skill cells, each with a numbered header. On desktop, the columns are stagger-offset vertically to create a staircase appearance.
14.1 Section Background
.skills {
position: relative;
padding: var(--section-pad) var(--side-pad) calc(var(--section-pad) + 2rem);
z-index: 5;
background: var(--warm-white);
border-top: 1px solid rgba(196, 149, 106, 0.14);
}
14.2 Three-Column Grid with Staircase Offset
.skills__columns {
max-width: var(--content-wide);
margin: 2.5rem auto 0;
display: grid;
grid-template-columns: 1fr;
gap: 2.5rem;
}
@media (min-width: 820px) {
.skills__columns {
grid-template-columns: repeat(3, 1fr);
gap: 1.75rem;
}
.skills__column--1 { transform: translateY(0); }
.skills__column--2 { transform: translateY(2.2rem); }
.skills__column--3 { transform: translateY(4.4rem); }
}
14.3 Column Header
.skills__column-header {
display: flex;
align-items: baseline;
gap: 0.85rem;
padding-bottom: 0.85rem;
margin-bottom: 1.25rem;
border-bottom: 1px solid rgba(196, 149, 106, 0.3);
}
.skills__column-num {
font-family: var(--font-serif);
font-size: 1.8rem;
font-style: italic;
line-height: 1;
color: var(--terracotta);
opacity: 0.85;
}
.skills__column-title {
font-family: var(--font-serif);
font-weight: 400;
font-size: 1.15rem;
letter-spacing: 0.04em;
color: var(--charcoal);
}
.skills__column-subtitle {
font-family: var(--font-sans);
font-size: 0.68rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--warm-grey);
}
14.4 Skill Cell
.skill-cell {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.95rem;
padding: 0.9rem 1.1rem;
background: var(--card-glass);
border: 1px solid var(--card-border);
border-radius: 8px;
cursor: default;
transition:
transform 0.35s var(--ease-out),
border-color 0.35s var(--ease-out),
background 0.35s var(--ease-out),
box-shadow 0.35s var(--ease-out);
}
.skill-cell:hover {
border-color: var(--terracotta);
background: var(--card-glass-hover);
transform: translateX(4px); /* Slides right — consistent with about__point hover */
box-shadow: 0 4px 14px rgba(196, 149, 106, 0.1);
}
.skill-cell__icon {
width: 26px; height: 26px;
flex-shrink: 0;
color: var(--charcoal);
opacity: 0.65;
transition: opacity 0.35s var(--ease-out), transform 0.35s var(--ease-out), color 0.35s var(--ease-out);
}
.skill-cell:hover .skill-cell__icon {
opacity: 1;
color: var(--terracotta);
transform: scale(1.1);
}
.skill-cell__name {
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 500;
letter-spacing: 0.01em;
color: var(--soft-black);
}
.skill-cell__tag {
font-family: var(--font-sans);
font-size: 0.68rem;
color: var(--warm-grey);
}
Section 15 — Projects Section
Alternating left-right large project cards with SVG illustrations and narrative descriptions.
15.1 Project Card Layout
.project-card {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
padding: 2rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
@media (min-width: 768px) {
.project-card {
grid-template-columns: 1fr 1fr;
gap: clamp(2rem, 4vw, 4rem);
align-items: start;
}
/* Flip every other card — illustration on right for even, left for odd */
.project-card:nth-child(even) { direction: rtl; }
.project-card:nth-child(even) > * { direction: ltr; }
}
15.2 Project Illustration
.project-card__illustration {
opacity: 0.65;
transition: opacity 0.6s var(--ease-out);
}
.project-card:hover .project-card__illustration {
opacity: 1;
}
.project-illu {
width: 100%;
height: auto;
max-height: 200px;
}
15.3 Project Typography
.project-card__title {
font-family: var(--font-serif);
font-weight: 400;
font-size: clamp(1.3rem, 2.5vw, 1.8rem);
letter-spacing: 0.04em;
color: var(--charcoal);
margin-bottom: 1rem;
}
.project-card__description {
font-size: clamp(0.88rem, 1.2vw, 0.95rem);
line-height: 1.8;
color: var(--soft-black);
}
.project-card__description a {
color: var(--terracotta);
text-decoration: none;
border-bottom: 1px solid rgba(184, 122, 90, 0.3);
transition: border-color 0.25s var(--ease-out);
}
.project-card__description a:hover {
border-bottom-color: var(--terracotta);
border-bottom-width: 1.5px;
}
.project-card__caption {
font-family: var(--font-serif);
font-size: 0.85rem;
color: var(--warm-grey);
font-style: italic;
letter-spacing: 0.03em;
}
Section 16 — Learning Section (Compact Cards)
A two-column split grid: articles on the left, videos on the right. Each entry is a small horizontal card with a 48×48px SVG thumbnail.
16.1 Compact Card
.compact-card {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.7rem;
padding: 0.55rem 0.8rem;
background: var(--card-glass);
border: 1px solid var(--card-border);
border-radius: 8px;
text-decoration: none;
color: inherit;
backdrop-filter: blur(8px);
transition:
transform 0.25s var(--ease-out),
border-color 0.25s var(--ease-out),
background 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.compact-card:hover {
transform: translateX(2px) translateY(-1px);
border-color: var(--terracotta);
background: var(--card-glass-hover);
box-shadow: 0 4px 14px rgba(196, 149, 106, 0.12);
}
.compact-card__icon {
flex-shrink: 0;
width: 48px; height: 48px;
border-radius: 6px;
background: var(--card-item-bg);
border: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.compact-card__tag {
font-family: var(--font-sans);
font-size: 0.58rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 600;
}
.compact-card__platform {
font-family: var(--font-sans);
font-size: 0.58rem;
color: var(--warm-grey);
border-left: 1px solid rgba(196, 149, 106, 0.3);
padding-left: 0.45rem;
}
.compact-card__title {
font-family: var(--font-serif);
font-weight: 400;
font-size: 0.88rem;
line-height: 1.25;
color: var(--charcoal);
}
.compact-card__desc {
font-size: 0.72rem;
line-height: 1.4;
color: var(--soft-black);
opacity: 0.85;
}
.compact-card__cta {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.7rem;
font-weight: 500;
color: var(--terracotta);
}
Section 17 — Ambient Particles (Canvas)
A fixed canvas behind all content renders soft gold particles that drift slowly, connected by faint lines when within 120px proximity. This creates the "constellation" atmosphere.
function initParticles() {
const canvas = document.getElementById('particles-canvas');
const ctx = canvas.getContext('2d');
const PARTICLE_COUNT = window.innerWidth < 768 ? 18 : 35;
// Each particle: { x, y, r (0.3–1.5), vx (~±0.075), vy (up-biased), opacity, life }
// Gold fill: rgba(196, 149, 106, opacity * fadeRatio)
// Connections: draw line between particles < 120px apart
// strokeStyle: rgba(196, 149, 106, (1 - dist/120) * 0.06)
// lineWidth: 0.4px
// Pause loop on document.hidden (tab visibility API)
// Resize canvas on window resize, debounced 150ms
}
Canvas CSS:
#particles-canvas {
position: fixed;
inset: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 1;
opacity: 0.4;
transform: translateZ(0);
will-change: transform;
}
In Flutter: Use CustomPainter with Canvas.drawCircle and Canvas.drawLine. Use a Ticker or AnimationController for the animation loop. Pause via WidgetsBindingObserver when app goes to background.
Section 18 — Resume Button and Modal
18.1 Open Button
A pill-shaped dark button — the only instance of a solid-background dark button in the entire UI:
.resume-open-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.65rem 1.4rem 0.65rem 1rem;
background: var(--charcoal);
color: var(--warm-white);
border: none;
border-radius: 999px;
font-family: var(--font-sans);
font-size: 0.82rem;
font-weight: 500;
cursor: pointer;
transition: background 0.22s var(--ease-out), transform 0.22s var(--ease-out), box-shadow 0.22s var(--ease-out);
}
.resume-open-btn:hover {
background: var(--soft-black);
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(26, 26, 26, 0.18);
}
18.2 Modal
.resume-modal {
position: fixed;
inset: 0;
z-index: 2000;
display: flex;
align-items: stretch;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s var(--ease-out);
}
.resume-modal:not([hidden]) { opacity: 1; pointer-events: auto; }
.resume-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(26, 26, 26, 0.72);
backdrop-filter: blur(6px);
}
.resume-modal__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(960px, 96vw);
height: 96vh;
margin: 2vh auto;
background: var(--warm-white);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 24px 80px rgba(26, 26, 26, 0.35);
transform: translateY(12px);
transition: transform 0.32s var(--ease-out);
}
.resume-modal:not([hidden]) .resume-modal__panel {
transform: translateY(0);
}
Section 19 — Flutter / Dart Implementation Guide
19.1 Color System
Define all tokens in a AppColors class with static constants, with separate light and dark instances:
class AppColors {
// Light Mode
static const cream = Color(0xFFF7F5F0);
static const sage = Color(0xFFEEF3ED);
static const parchment = Color(0xFFF1ECE3);
static const warmWhite = Color(0xFFFAF8F5);
static const linen = Color(0xFFF3EFE8);
static const sageTint = Color(0xFFEFF3EE);
static const charcoal = Color(0xFF1A1A1A);
static const softBlack = Color(0xFF2C2C2C);
static const warmGrey = Color(0xFF8A8580);
static const lightGrey = Color(0xFFC5BFB8);
static const terracotta = Color(0xFFB87A5A);
static const gold = Color(0xFFC4956A);
static const goldDim = Color(0x4DC4956A);
static const blush = Color(0xFFF0EBE5);
// Dark Mode equivalents
static const darkCream = Color(0xFF141210);
static const darkSage = Color(0xFF171816);
static const darkParchment = Color(0xFF1A1815);
static const darkWarmWhite = Color(0xFF1E1B18);
static const darkLinen = Color(0xFF181614);
static const darkSageTint = Color(0xFF161815);
static const darkCharcoal = Color(0xFFEDE8E1);
static const darkSoftBlack = Color(0xFFD8D2C8);
static const darkWarmGrey = Color(0xFF9E978F);
static const darkTerracotta = Color(0xFFD98A62);
static const darkGold = Color(0xFFD4A373);
}
19.2 Typography
Use google_fonts package:
import 'package:google_fonts/google_fonts.dart';
final serifStyle = GoogleFonts.cormorantGaramond(
fontWeight: FontWeight.w300,
letterSpacing: 0.06 * 16,
);
final sansStyle = GoogleFonts.inter(
fontWeight: FontWeight.w300,
height: 1.7,
);
19.3 Theme Data
ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
scaffoldBackgroundColor: AppColors.cream,
colorScheme: ColorScheme.light(
primary: AppColors.gold,
secondary: AppColors.terracotta,
surface: AppColors.warmWhite,
onSurface: AppColors.charcoal,
),
textTheme: TextTheme(
displayLarge: GoogleFonts.cormorantGaramond(
fontSize: 56, fontWeight: FontWeight.w300,
letterSpacing: 2.4, color: AppColors.charcoal,
),
headlineMedium: GoogleFonts.cormorantGaramond(
fontSize: 28, fontWeight: FontWeight.w300,
letterSpacing: 1.0, color: AppColors.charcoal,
),
bodyMedium: GoogleFonts.inter(
fontSize: 14, fontWeight: FontWeight.w300,
height: 1.7, color: AppColors.softBlack,
),
labelSmall: GoogleFonts.inter(
fontSize: 11, fontWeight: FontWeight.w600,
letterSpacing: 1.3, color: AppColors.terracotta,
),
),
);
19.4 Card Component Pattern
class NotebookCard extends StatefulWidget {
final Widget child;
final VoidCallback? onTap;
const NotebookCard({required this.child, this.onTap, super.key});
@override
State<NotebookCard> createState() => _NotebookCardState();
}
class _NotebookCardState extends State<NotebookCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOut,
transform: Matrix4.translationValues(_hovered ? 4.0 : 0, 0, 0),
decoration: BoxDecoration(
color: _hovered
? (isDark ? const Color(0xFF2A2622) : Colors.white)
: (isDark ? const Color(0xFF1E1B18).withOpacity(0.72) : Colors.white.withOpacity(0.65)),
border: Border.all(
color: _hovered
? AppColors.goldDim.withOpacity(0.45)
: AppColors.goldDim.withOpacity(0.22),
),
borderRadius: BorderRadius.circular(8),
boxShadow: _hovered ? [
BoxShadow(
color: AppColors.gold.withOpacity(0.12),
blurRadius: 14, offset: const Offset(0, 4),
),
] : [],
),
child: widget.child,
),
),
);
}
}
19.5 Scroll-Reveal Animation
class RevealItem extends StatefulWidget {
final Widget child;
final Duration delay;
const RevealItem({required this.child, this.delay = Duration.zero, super.key});
@override
State<RevealItem> createState() => _RevealItemState();
}
class _RevealItemState extends State<RevealItem>
with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
late Animation<double> _opacity;
late Animation<Offset> _slide;
bool _visible = false;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_opacity = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut);
_slide = Tween(begin: const Offset(0, 0.1), end: Offset.zero)
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
}
void _onVisibilityChanged(bool visible) {
if (visible && !_visible) {
_visible = true;
Future.delayed(widget.delay, () {
if (mounted) _ctrl.forward();
});
}
}
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: key ?? UniqueKey(),
onVisibilityChanged: (info) => _onVisibilityChanged(info.visibleFraction > 0.1),
child: FadeTransition(
opacity: _opacity,
child: SlideTransition(offset: _slide, child: widget.child),
),
);
}
@override
void dispose() { _ctrl.dispose(); super.dispose(); }
}
19.6 Section Title Widget
class SectionTitle extends StatelessWidget {
final String text;
const SectionTitle(this.text, {super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
text,
style: GoogleFonts.cormorantGaramond(
fontSize: 42,
fontWeight: FontWeight.w300,
letterSpacing: 0.06 * 42,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
Container(
width: 40,
height: 1,
color: AppColors.gold.withOpacity(0.5),
),
],
);
}
}
19.7 Skill Cell Widget
class SkillCell extends StatefulWidget {
final IconData? icon;
final Widget? customIcon;
final String name;
final String tag;
const SkillCell({this.icon, this.customIcon, required this.name, required this.tag, super.key});
@override
State<SkillCell> createState() => _SkillCellState();
}
class _SkillCellState extends State<SkillCell> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOut,
transform: Matrix4.translationValues(_hovered ? 4.0 : 0, 0, 0),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: _hovered ? Colors.white : Colors.white.withOpacity(0.65),
border: Border.all(
color: _hovered
? AppColors.terracotta.withOpacity(0.6)
: AppColors.gold.withOpacity(0.22),
),
borderRadius: BorderRadius.circular(8),
boxShadow: _hovered ? [
BoxShadow(
color: AppColors.gold.withOpacity(0.1),
blurRadius: 14, offset: const Offset(0, 4),
)
] : [],
),
child: Row(
children: [
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 350),
style: TextStyle(
color: _hovered ? AppColors.terracotta : AppColors.charcoal,
),
child: Opacity(
opacity: _hovered ? 1.0 : 0.65,
child: SizedBox(
width: 26, height: 26,
child: widget.customIcon ?? Icon(widget.icon, size: 20),
),
),
),
const SizedBox(width: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.name,
style: GoogleFonts.inter(
fontSize: 13.6, fontWeight: FontWeight.w500,
letterSpacing: 0.16, color: AppColors.softBlack,
),
),
const SizedBox(height: 2),
Text(widget.tag,
style: GoogleFonts.inter(
fontSize: 10.9, color: AppColors.warmGrey,
),
),
],
),
],
),
),
);
}
}
Section 20 — Anti-Patterns Reference
The following must never appear in this design system. If a UI element matches any of these descriptions, it is wrong and must be corrected:
| Anti-Pattern | Correct Approach |
|---|---|
| Purple-to-pink gradient backgrounds | Warm neutral surfaces: --cream, --sage, --parchment |
Cards with identical height forced by height: 100% | Cards grow to fit their content; vertical rhythm comes from spacing, not equal height |
| Neon glow on hover | Gold box-shadow at 10–15% opacity maximum |
| Corner radius > 14px on content cards | 8–12px for cards; 999px only for pills/buttons/chips |
| Color-coded skill progress bars | Skill cells with SVG icons and two lines of text |
| Timeline with bullet-point lists | Vertical thread, circular SVG markers, illustrated right column |
| Emoji as decoration | Unicode geometric characters (✦) used sparingly as accent only |
| Font weight > 600 for headings | Cormorant Garamond weight 300 or 400 — this face is not bold |
letter-spacing tighter than 0 | Always use positive tracking; this is an editorial typeface |
Gradient text (background-clip: text) | Plain var(--charcoal) or var(--terracotta) text |
| SVG icons from icon libraries (heroicons, lucide) | Hand-drawn SVG paths with 0.6–0.9px stroke weight |
| Backdrop blur > 12px on floating elements | 4–10px blur — heavy blur obscures the page texture |
| z-index above 9999 (except modals at 2000) | Grain overlay is 9999; stay well below unless opening a modal |
Section 21 — Responsive Breakpoints
| Breakpoint | Behavior |
|---|---|
< 600px | Hero floats scale to 65%; theme toggle text hidden; waypoint tooltips reposition below dot |
< 720px | Learning section collapses to 1 column |
< 768px | Project cards collapse to 1 column; timeline padding adjusts |
< 820px | Skills staircase collapses to 1 column |
< 860px | About section collapses to 1 column |
< 960px | Journey rail navigation hidden |
>= 1400px | Waypoint positions increase to prevent drift |
Section 22 — Accessibility Requirements
- All interactive elements must have
focus-visibleoutlines:2px solid var(--gold)at3px offset - Journey waypoints have
tabindex="0"and descriptivearia-labelattributes - Floating items have
aria-hidden="true"where they carry no semantic information - Resume modal uses
role="dialog",aria-modal="true", andaria-labelledby - Theme toggle has
aria-labelthat updates dynamically: "Switch to dark theme" / "Switch to light theme" - Paper grain overlay and canvas elements carry
aria-hidden="true" @media (prefers-reduced-motion: reduce)disables all transitions and animations immediately withtransition: none !important- Color contrast for body text (
#1A1A1Aon#F7F5F0): 16.1:1 — well above WCAG AAA - Color contrast for muted text (
#8A8580on#F7F5F0): 4.6:1 — passes WCAG AA
Section 23 — Implementation Checklist
Before shipping any implementation of this design system, verify:
- Flash-free theme detection script is the first script in
<head>, before any CSS - Both
prefers-color-scheme: darkmedia query ANDdata-theme="dark"attribute are handled - All 20 CSS custom properties are defined for both light and dark modes
- SVG stroke override selectors are present for
#1A1A1Aand#2C2C2C -
prefers-reduced-motionmedia query disables all animations - Grain overlay is
pointer-events: noneandz-index: 9999 - Journey line and rail are
z-index: 6and1000respectively, never submerging under sections - All section backgrounds use the correct distinct surface token (not all
--cream) - Section titles have the 40px × 1px gold underline rule
- Skill cells slide right (+4px translateX) on hover, not up
- About points slide right (+4px) on hover; ✦ bullet rotates 45° on hover
- Project cards alternate direction with
direction: rtltrick - Compact cards have a 48×48px icon thumbnail, not text-only
- Resume modal enters with
translateY(12px → 0)and exits with opacity fade -
font-family: var(--font-serif)is applied to all headings, section titles, chips, labels, waypoint messages, and rail tooltips -
font-weight: 300is the default weight for Cormorant Garamond in all heading contexts - In Flutter:
VisibilityDetectoror scroll observer is used for reveal animations, notinitState - In Flutter:
ThemeModepersisted toSharedPreferenceswith keycolor-scheme