stable: finalized GIS aesthetics and Syria/Jordan routing synchronization
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
@@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Map SaaS | Master-Class Tutorial</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&family=Noto+Sans+Arabic:wght@400;700;800&family=Fira+Code&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-glow"></div>
|
||||
<div class="app-container">
|
||||
<aside>
|
||||
<div class="brand">
|
||||
<div class="brand-icon">M</div>
|
||||
<h2>Master-Class</h2>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#intro" class="active">01. Sovereignty</a></li>
|
||||
<li><a href="#acquisition">02. Data Acquisition</a></li>
|
||||
<li><a href="#engine">03. The Spatial Engine</a></li>
|
||||
<li><a href="#import">04. Mastering Import</a></li>
|
||||
<li><a href="#serving">05. Serving Geometry</a></li>
|
||||
<li><a href="#routing">06. Intelligent Routing</a></li>
|
||||
<li><a href="#interface">07. Modern Interface</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<header class="hero" id="intro">
|
||||
<img src="hero.png" alt="Map Data Hero" class="hero-img">
|
||||
<div class="hero-content">
|
||||
<span class="badge">Advanced Tutorial</span>
|
||||
<h1>Building the Future of Maps</h1>
|
||||
<p>A ground-up engineering guide to creating a fully sovereign mapping infrastructure.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="sections-wrapper">
|
||||
<!-- 01. Sovereignty -->
|
||||
<section id="intro">
|
||||
<span class="section-label">Concept</span>
|
||||
<h2>Digital Sovereignty</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>In the age of expensive APIs and data lock-ins, building your own map stack isn't just a technical choice—it's a declaration of <strong>Sovereignty</strong>. By orchestrating OpenStreetMap, PostGIS, and high-performance tile servers, we eliminate per-view costs and gain absolute control over every pixel.</p>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>في عصر واجهات برمجة التطبيقات المكلفة وتقييد البيانات، لا يعد بناء مجموعة أدوات الخرائط الخاصة بك مجرد خيار تقني، بل هو إعلان عن <strong>السيادة الرقمية</strong>. من خلال تنظيم OpenStreetMap وPostGIS وخوادم البلاطات عالية الأداء، نقوم بإلغاء تكاليف العرض لكل خريطة ونكتسب تحكماً مطلقاً في كل بكسل.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02. Data Acquisition -->
|
||||
<section id="acquisition">
|
||||
<span class="section-label">Source</span>
|
||||
<h2>Phase 1: Raw Data Acquisition</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>Where does the world's map data come from? For this project, we rely on <strong>OpenStreetMap (OSM)</strong>. We obtain high-quality, pre-extracted geographic fragments from <strong>Geofabrik</strong>.</p>
|
||||
<div class="highlight-box">
|
||||
<h4>Why Geofabrik?</h4>
|
||||
<p>Downloading the entire world is 1.6TB. Geofabrik allows us to target specific regions (like Jordan) in the `.osm.pbf` format—a highly compressed binary structure.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>من أين تأتي بيانات خرائط العالم؟ في هذا المشروع، نعتمد على <strong>OpenStreetMap (OSM)</strong>. نحصل على قطع جغرافية عالية الجودة ومستخرجة مسبقاً من <strong>Geofabrik</strong>.</p>
|
||||
<div class="highlight-box">
|
||||
<h4>لماذا Geofabrik؟</h4>
|
||||
<p>تحميل العالم بالكامل يزن 1.6 تيرابايت. يسمح لنا Geofabrik باستهداف مناطق معينة (مثل الأردن) بتنسيق `.osm.pbf` - وهو هيكل ثنائي مضغوط للغاية.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Source URL: <code>http://download.geofabrik.de/asia/jordan-latest.osm.pbf</code></p>
|
||||
</section>
|
||||
|
||||
<!-- 03. The Spatial Engine -->
|
||||
<section id="engine">
|
||||
<span class="section-label">Infrastructure</span>
|
||||
<h2>Phase 2: The Spatial Database</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>Standard databases store text and numbers. A mapping engine needs to store <strong>Geometry</strong>. We use **PostGIS**, an extension for PostgreSQL that adds support for geographic objects.</p>
|
||||
<div class="highlight-box">
|
||||
<h4>The "Why" of PostGIS</h4>
|
||||
<p>PostGIS allows us to perform spatial SQL queries. For example, finding all roads that intersect with a specific park, or calculating the area of a building block instantly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>قواعد البيانات العادية تخزن النصوص والأرقام. أما محرك الخرائط فيحتاج إلى تخزين <strong>الهندسة (Geometry)</strong>. نحن نستخدم **PostGIS**، وهي إضافة لـ PostgreSQL تضيف دعماً للأجسام الجغرافية.</p>
|
||||
<div class="highlight-box">
|
||||
<h4>سبب استخدام PostGIS</h4>
|
||||
<p>يسمح لنا PostGIS بإجراء استعلامات SQL مكانية. على سبيل المثال، العثور على جميع الطرق التي تتقاطع مع حديقة معينة، أو حساب مساحة كتلة بناء بشكل فوري.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 04. Mastering Import -->
|
||||
<section id="import">
|
||||
<span class="section-label">Processing</span>
|
||||
<h2>Phase 3: Data Transformation</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>To move data from a PBF file into PostGIS, we use <strong>osm2pgsql</strong>. This isn't just a copy-paste tool; it's a sophisticated translator.</p>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>لنقل البيانات من ملف PBF إلى PostGIS، نستخدم <strong>osm2pgsql</strong>. هذه ليست مجرد أداة نسخ ولصق؛ بل هي مترجم متطور.</p>
|
||||
</div>
|
||||
</div>
|
||||
<pre><code>osm2pgsql --create --slim --cache 1000 --database map_db --host db jordan-latest.osm.pbf</code></pre>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-card">
|
||||
<h3>--create</h3>
|
||||
<p>Initializes a fresh schema. Wipe out old data and start clean.</p>
|
||||
<p>(AR) لإنشاء مخطط (Schema) جديد تماماً ومسح البيانات القديمة.</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<h3>--slim</h3>
|
||||
<p>Stores intermediate data on disk. Essential for keeping RAM usage low during complex imports.</p>
|
||||
<p>(AR) لتخزين البيانات الوسيطة على القرص، مما يحافظ على استهلاك الذاكرة منخفضاً.</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<h3>--cache</h3>
|
||||
<p>Allocates RAM (1GB here) to the tool. More cache means faster translation from PBF to SQL.</p>
|
||||
<p>(AR) يخصص ذاكرة وصول عشوائي للأداة. ذاكرة أكبر تعني ترجمة أسرع للبيانات.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 05. Serving Geometry -->
|
||||
<section id="serving">
|
||||
<span class="section-label">Real-time</span>
|
||||
<h2>Phase 4: Vector Tile Serving</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>Mapping in 2026 relies on <strong>Vector Tiles</strong>. Instead of static pictures, we send mathematical geometry to the browser. We use <strong>Martin</strong> because it generates these tiles dynamically from PostGIS SQL tables.</p>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>تعتمد الخرائط في عام 2026 على <strong>بلاطات المتجهات (Vector Tiles)</strong>. بدلاً من الصور الثابتة، نرسل أجزاء هندسية رياضية إلى المتصفح. نستخدم <strong>Martin</strong> لأنه ينشئ هذه البلاطات ديناميكياً من جداول SQL.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 06. Intelligent Routing -->
|
||||
<section id="routing">
|
||||
<span class="section-label">Logistics</span>
|
||||
<h2>Phase 5: Pathfinding with GraphHopper</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>Maps are for travel. <strong>GraphHopper</strong> is our logistics brain. It reads the imported road network and applies Dijkstra-based algorithms to find the fastest path between any two points in Jordan.</p>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>الخرائط هي للسفر والتحرك. <strong>GraphHopper</strong> هو دماغ اللوجستيات الخاص بنا. يقرأ شبكة الطرق المستوردة ويطبق خوارزميات (Dijkstra) للعثور على أسرع مسار بين أي نقطتين في الأردن.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 07. Modern Interface -->
|
||||
<section id="interface">
|
||||
<span class="section-label">Interaction</span>
|
||||
<h2>Phase 6: The Full-Stack Bridge</h2>
|
||||
<div class="deep-dive">
|
||||
<div class="en-side">
|
||||
<p>Last, we bridge it all. <strong>NestJS</strong> provides the secured API proxy, and <strong>React + MapLibre GL</strong> provides the high-performance visualization layer.</p>
|
||||
</div>
|
||||
<div class="ar-side">
|
||||
<p>أخيراً، نجمع كل شيء. يوفر <strong>NestJS</strong> بوابة آمنة (API Proxy)، ويوفر <strong>React + MapLibre GL</strong> طبقة التصور المرئي عالية الأداء.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer style="margin-top: 10rem; padding-top: 4rem; border-top: 1px solid var(--border); text-align: center;">
|
||||
<p>© 2026 Map SaaS Engineering Tutorial. All Rights Reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const navLinks = document.querySelectorAll('nav a');
|
||||
const sections = document.querySelectorAll('section');
|
||||
|
||||
// Smooth scroll with offset for sticky header/sidebar
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const targetId = link.getAttribute('href').substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
window.scrollTo({
|
||||
top: targetElement.offsetTop - 50,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Intersection Observer for Sidebar Active State
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-20% 0px -70% 0px',
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.getAttribute('id');
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === `#${id}`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
sections.forEach(section => observer.observe(section));
|
||||
|
||||
// Advanced Reveal Animations on Scroll
|
||||
const revealElements = document.querySelectorAll('.deep-dive, .tech-card, .highlight-box, pre');
|
||||
|
||||
const revealObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('revealed');
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
revealElements.forEach(el => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateY(40px)';
|
||||
el.style.transition = 'all 1s cubic-bezier(0.2, 0.8, 0.2, 1)';
|
||||
revealObserver.observe(el);
|
||||
});
|
||||
|
||||
// CSS injection for reveal animation class
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.revealed {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Parallax Hero Effect
|
||||
window.addEventListener('scroll', () => {
|
||||
const hero = document.querySelector('.hero-img');
|
||||
const scroll = window.pageYOffset;
|
||||
if (hero) {
|
||||
hero.style.transform = `scale(1.1) translateY(${scroll * 0.4}px)`;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-glow: rgba(99, 102, 241, 0.4);
|
||||
--secondary: #06b6d4;
|
||||
--accent: #f59e0b;
|
||||
--bg-dark: #020617;
|
||||
--bg-card: rgba(15, 23, 42, 0.8);
|
||||
--text-main: #f1f5f9;
|
||||
--text-dim: #94a3b8;
|
||||
--glass: rgba(255, 255, 255, 0.03);
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--font-en: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-ar: 'Noto Sans Arabic', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-en);
|
||||
line-height: 1.7;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(to bottom, var(--primary), var(--secondary));
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Background Elements */
|
||||
.bg-glow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.05) 0%, transparent 40%),
|
||||
radial-gradient(circle at 90% 80%, rgba(6, 118, 212, 0.05) 0%, transparent 40%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.app-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar Navigation */
|
||||
aside {
|
||||
width: 320px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(20px);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 3rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 20px var(--primary-glow);
|
||||
}
|
||||
|
||||
.brand h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--text-dim);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
nav a:hover, nav a.active {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background: linear-gradient(to right, rgba(99, 102, 241, 0.1), transparent);
|
||||
border-left: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(to top, var(--bg-dark), transparent);
|
||||
}
|
||||
|
||||
.hero-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: brightness(0.4) saturate(1.2);
|
||||
transform: scale(1.1);
|
||||
transition: transform 20s ease-out;
|
||||
}
|
||||
|
||||
.hero:hover .hero-img {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 1.5rem;
|
||||
background: linear-gradient(to bottom, #fff, var(--text-dim));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary-glow);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 50px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Tutorial Sections */
|
||||
.sections-wrapper {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 6rem 2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 10rem;
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.75rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 3rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
/* Detailed Bilingual Blocks */
|
||||
.deep-dive {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.en-side, .ar-side {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ar-side {
|
||||
direction: rtl;
|
||||
font-family: var(--font-ar);
|
||||
}
|
||||
|
||||
.deep-dive p {
|
||||
font-size: 1.15rem;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2rem;
|
||||
border-radius: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.highlight-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* Grid Cards */
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.tech-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2.5rem;
|
||||
border-radius: 2rem;
|
||||
transition: all 0.5s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tech-card:hover {
|
||||
transform: translateY(-10px);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.tech-card i {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 1.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Code Mirror Styles */
|
||||
pre {
|
||||
background: #010409;
|
||||
padding: 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
margin: 3rem 0;
|
||||
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.95rem;
|
||||
color: #e6edf3;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.deep-dive {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
aside {
|
||||
display: none;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user